diff --git a/DEPS b/DEPS
index a0c8bdd..91cb8072 100644
--- a/DEPS
+++ b/DEPS
@@ -253,19 +253,19 @@
   # 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': '55ec34727676ec9a3bcd9aea00d58ae5a4e1d11c',
+  'skia_revision': '2ac7682b5303c11485ccc48f8e6264b27f102850',
   # 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': 'ad2e42379a0b772da47db66e251d173ec1fb63a3',
+  'v8_revision': 'd2ce11ad0810831476da44a8f0a317078076f61b',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling ANGLE
   # and whatever else without interference from each other.
-  'angle_revision': 'f2e7a2359c5daf22719ffcac3fafb43d735baed3',
+  'angle_revision': '041c4c6d285c571af0b3bc63567429043b70146b',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling SwiftShader
   # and whatever else without interference from each other.
-  'swiftshader_revision': '205ddee16e5f6fe673e2fdb64510034ce4c9ad03',
+  'swiftshader_revision': '3ed03de5e79de515b3dc9f43bdb4c766ad2c5a20',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling PDFium
   # and whatever else without interference from each other.
@@ -328,7 +328,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': '90f7ed24b9e2502844d26311760cab27495cd6b6',
+  'devtools_frontend_revision': '5d6735af024d429f24f64e2c0b0de1408cdd5cbc',
   # 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.
@@ -368,7 +368,7 @@
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling feed
   # and whatever else without interference from each other.
-  'dawn_revision': 'f2556ab35c0eecdfd93c02f7c226a5c94316d143',
+  'dawn_revision': '57b7db9c7425cb4de5c7533c200e96d56fe2011b',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling feed
   # and whatever else without interference from each other.
@@ -435,7 +435,7 @@
   'libcxx_revision':       '79a2e924d96e2fc1e4b937c42efd08898fa472d7',
 
   # GN CIPD package version.
-  'gn_version': 'git_revision:bd99dbf98cbdefe18a4128189665c5761263bcfb',
+  'gn_version': 'git_revision:ff14fc1112e0a8dd2c3910fb89539741cb3d3f23',
 }
 
 # Only these hosts are allowed for dependencies in this DEPS file.
@@ -728,7 +728,7 @@
   },
 
   'src/ios/third_party/earl_grey2/src': {
-      'url': Var('chromium_git') + '/external/github.com/google/EarlGrey.git' + '@' + 'aaf6cd0daad5e447754d89141fb75a2a0c4cee9a',
+      'url': Var('chromium_git') + '/external/github.com/google/EarlGrey.git' + '@' + '300146f8abf67e41bc6af894e38c9ae88404212b',
       'condition': 'checkout_ios',
   },
 
@@ -1351,7 +1351,7 @@
     Var('chromium_git') + '/external/libaddressinput.git' + '@' + '3b8ee157a8f3536bbf5ad2448e9e3370463c1e40',
 
   'src/third_party/libaom/source/libaom':
-    Var('aomedia_git') + '/aom.git' + '@' +  'ee1ed1ccf2b9ecedd6aee438eafc7cc61c23342d',
+    Var('aomedia_git') + '/aom.git' + '@' +  '24fa287e152b319d8998e24c0f174f4043138bfd',
 
   'src/third_party/libavif/src':
     Var('chromium_git') + '/external/github.com/AOMediaCodec/libavif.git' + '@' + Var('libavif_revision'),
@@ -1415,7 +1415,7 @@
     Var('chromium_git') + '/webm/libwebm.git' + '@' + 'e4fbea0c9751ae8aa86629b197a28d8276a2b0da',
 
   'src/third_party/libyuv':
-    Var('chromium_git') + '/libyuv/libyuv.git' + '@' + '3aebf69d668177e7ee6dbbe0025e5c3dbb525ff2',
+    Var('chromium_git') + '/libyuv/libyuv.git' + '@' + 'f4d25308467cbd50c2706a46fa0ddcef939e715a',
 
   'src/third_party/lighttpd': {
       'url': Var('chromium_git') + '/chromium/deps/lighttpd.git' + '@' + Var('lighttpd_revision'),
@@ -1741,10 +1741,10 @@
     Var('chromium_git') + '/external/khronosgroup/webgl.git' + '@' + 'cf04aebdf9b53bb2853f22a81465688daf879ec6',
 
   'src/third_party/webgpu-cts/src':
-    Var('chromium_git') + '/external/github.com/gpuweb/cts.git' + '@' + '68fbc0f8f30c00408fb94807c197f055e5535795',
+    Var('chromium_git') + '/external/github.com/gpuweb/cts.git' + '@' + 'cc77d560f4feb725c1172306dd0943b44838c65f',
 
   'src/third_party/webrtc':
-    Var('webrtc_git') + '/src.git' + '@' + '3cdd653d6691c145ac1e102224c7cd48a56c2ca5',
+    Var('webrtc_git') + '/src.git' + '@' + '2a83a9c5cd9f15a997d6d5857bce9ad11c2f1682',
 
   'src/third_party/libgifcodec':
      Var('skia_git') + '/libgifcodec' + '@'+  Var('libgifcodec_revision'),
@@ -1814,7 +1814,7 @@
     Var('chromium_git') + '/v8/v8.git' + '@' +  Var('v8_revision'),
 
   'src-internal': {
-    'url': 'https://chrome-internal.googlesource.com/chrome/src-internal.git@2ed604efd56355b3d6c8ce7e78fe07d59fbdc1be',
+    'url': 'https://chrome-internal.googlesource.com/chrome/src-internal.git@fdca61bde57b64c3eec8fcd10e5fd9ccbebce04d',
     'condition': 'checkout_src_internal',
   },
 
diff --git a/ash/BUILD.gn b/ash/BUILD.gn
index 9d0377f5..13b381d3 100644
--- a/ash/BUILD.gn
+++ b/ash/BUILD.gn
@@ -1252,6 +1252,8 @@
     "system/network/auto_connect_notifier.h",
     "system/network/cellular_setup_notifier.cc",
     "system/network/cellular_setup_notifier.h",
+    "system/network/fake_network_detailed_network_view.cc",
+    "system/network/fake_network_detailed_network_view.h",
     "system/network/fake_network_detailed_view_delegate.cc",
     "system/network/fake_network_detailed_view_delegate.h",
     "system/network/network_detailed_network_view.cc",
@@ -1279,6 +1281,10 @@
     "system/network/network_info_bubble.h",
     "system/network/network_list_view.cc",
     "system/network/network_list_view.h",
+    "system/network/network_list_view_controller.cc",
+    "system/network/network_list_view_controller.h",
+    "system/network/network_list_view_controller_impl.cc",
+    "system/network/network_list_view_controller_impl.h",
     "system/network/network_observer.h",
     "system/network/network_row_title_view.cc",
     "system/network/network_row_title_view.h",
@@ -2114,6 +2120,7 @@
     "//ash/services/recording",
     "//ash/services/recording/public/mojom",
     "//ash/system/machine_learning:user_settings_event_proto",
+    "//ash/webui/eche_app_ui/mojom:mojom",
     "//ash/webui/personalization_app/mojom",
     "//ash/webui/personalization_app/proto",
     "//base",
@@ -2601,6 +2608,8 @@
     "system/eche/eche_icon_loading_indicator_view_unittest.cc",
     "system/eche/eche_tray_unittest.cc",
     "system/firmware_update/firmware_update_notification_controller_unittest.cc",
+    "system/geolocation/geolocation_controller_test_util.cc",
+    "system/geolocation/geolocation_controller_test_util.h",
     "system/geolocation/geolocation_controller_unittest.cc",
     "system/gesture_education/gesture_education_notification_controller_unittest.cc",
     "system/holding_space/holding_space_animation_registry_unittest.cc",
@@ -2640,6 +2649,7 @@
     "system/network/network_feature_pod_controller_unittest.cc",
     "system/network/network_icon_unittest.cc",
     "system/network/network_info_bubble_unittest.cc",
+    "system/network/network_list_view_controller_unittest.cc",
     "system/network/sms_observer_unittest.cc",
     "system/network/vpn_list_unittest.cc",
     "system/network/wifi_toggle_notification_controller_unittest.cc",
diff --git a/ash/capture_mode/capture_mode_camera_controller.cc b/ash/capture_mode/capture_mode_camera_controller.cc
index ae30f11..585cf34 100644
--- a/ash/capture_mode/capture_mode_camera_controller.cc
+++ b/ash/capture_mode/capture_mode_camera_controller.cc
@@ -22,6 +22,7 @@
 #include "media/capture/video/video_capture_device_descriptor.h"
 #include "ui/compositor/layer.h"
 #include "ui/compositor/scoped_layer_animation_settings.h"
+#include "ui/display/screen.h"
 #include "ui/gfx/geometry/point.h"
 #include "ui/gfx/geometry/point_conversions.h"
 #include "ui/gfx/geometry/rounded_corners_f.h"
@@ -112,16 +113,6 @@
   return iter == list.end() ? nullptr : &(*iter);
 }
 
-// Stacking the camera preview window on top of all children of its parent so
-// that it can show up in the recording above everything else.
-void StackingPreviewAtTop(views::Widget* preview_widget) {
-  DCHECK(preview_widget);
-  auto* preview_window = preview_widget->GetNativeWindow();
-  auto* parent = preview_window->parent();
-  DCHECK(parent);
-  parent->StackChildAtTop(preview_window);
-}
-
 std::unique_ptr<views::Widget> CreateCameraPreviewWidget(
     const gfx::Rect& bounds) {
   auto camera_preview_widget = std::make_unique<views::Widget>();
@@ -134,7 +125,6 @@
   params.child = true;
   params.name = "CameraPreviewWidget";
   camera_preview_widget->Init(std::move(params));
-  StackingPreviewAtTop(camera_preview_widget.get());
   return camera_preview_widget;
 }
 
@@ -272,10 +262,10 @@
   auto* parent = controller->GetCameraPreviewParentWindow();
   DCHECK(parent);
   auto* native_window = camera_preview_widget_->GetNativeWindow();
-  if (parent != native_window->parent()) {
+
+  if (parent != native_window->parent())
     views::Widget::ReparentNativeView(native_window, parent);
-    StackingPreviewAtTop(camera_preview_widget_.get());
-  }
+
   MaybeUpdatePreviewWidgetBounds();
 }
 
@@ -301,6 +291,10 @@
   }
 
   const gfx::Rect target_bounds = GetPreviewWidgetBounds();
+  const bool did_bounds_change =
+      target_bounds != GetCurrentBoundsMatchingConfineBoundsCoordinates();
+  if (!did_bounds_change)
+    return;
 
   if (animate) {
     ui::Layer* layer = camera_preview_widget_->GetLayer();
@@ -313,13 +307,21 @@
   } else {
     camera_preview_widget_->SetBounds(target_bounds);
   }
+
+  auto* controller = CaptureModeController::Get();
+  if (controller->IsActive())
+    controller->capture_mode_session()->OnCameraPreviewBoundsChanged();
 }
 
 void CaptureModeCameraController::StartDraggingPreview(
     const gfx::PointF& screen_location) {
+  is_drag_in_progress_ = true;
   previous_location_in_screen_ = screen_location;
 
-  is_drag_in_progress_ = true;
+  auto* controller = CaptureModeController::Get();
+  if (controller->IsActive())
+    controller->capture_mode_session()->OnCameraPreviewDragStarted();
+
   // Use cursor compositing instead of the platform cursor when dragging to
   // ensure the cursor is aligned with the camera preview.
   Shell::Get()->UpdateCursorCompositingEnabled();
@@ -364,9 +366,9 @@
   // Disable cursor compositing at the end of the drag.
   Shell::Get()->UpdateCursorCompositingEnabled();
 
-  // Make sure cursor is updated correctly after camera preview is snapped.
-  if (CaptureModeController::Get()->IsActive()) {
-    CaptureModeController::Get()->capture_mode_session()->UpdateCursor(
+  auto* controller = CaptureModeController::Get();
+  if (controller->IsActive()) {
+    controller->capture_mode_session()->OnCameraPreviewDragEnded(
         gfx::ToRoundedPoint(screen_location), is_touch);
   }
 }
diff --git a/ash/capture_mode/capture_mode_camera_unittests.cc b/ash/capture_mode/capture_mode_camera_unittests.cc
index 58e52cfe..49e321d 100644
--- a/ash/capture_mode/capture_mode_camera_unittests.cc
+++ b/ash/capture_mode/capture_mode_camera_unittests.cc
@@ -33,9 +33,11 @@
 #include "base/test/bind.h"
 #include "base/test/scoped_feature_list.h"
 #include "ui/base/l10n/l10n_util.h"
+#include "ui/compositor/layer.h"
 #include "ui/gfx/geometry/point.h"
 #include "ui/gfx/geometry/rect.h"
 #include "ui/gfx/geometry/size.h"
+#include "ui/gfx/geometry/vector2d.h"
 #include "ui/gfx/image/image_unittest_util.h"
 #include "ui/gfx/paint_vector_icon.h"
 #include "ui/views/widget/widget.h"
@@ -49,6 +51,8 @@
 constexpr char kDefaultCameraDisplayName[] = "Default Cam";
 constexpr char kDefaultCameraModelId[] = "0def:c000";
 
+constexpr float kOverlapOpacity = 0.1f;
+
 TestCaptureModeDelegate* GetTestDelegate() {
   return static_cast<TestCaptureModeDelegate*>(
       CaptureModeController::Get()->delegate_for_testing());
@@ -298,6 +302,20 @@
     EXPECT_EQ(resize_button->GetTooltipText(), expected_tooltip_text);
   }
 
+  // Select capture region by pressing and dragging the mouse.
+  void SelectCaptureRegion(const gfx::Rect& region, bool release_mouse = true) {
+    auto* controller = CaptureModeController::Get();
+    ASSERT_TRUE(controller->IsActive());
+    ASSERT_EQ(CaptureModeSource::kRegion, controller->source());
+    auto* event_generator = GetEventGenerator();
+    event_generator->set_current_screen_location(region.origin());
+    event_generator->PressLeftButton();
+    event_generator->MoveMouseTo(region.bottom_right());
+    if (release_mouse)
+      event_generator->ReleaseLeftButton();
+    EXPECT_EQ(region, controller->user_capture_region());
+  }
+
  private:
   base::test::ScopedFeatureList scoped_feature_list_;
   base::SystemMonitor system_monitor_;
@@ -684,23 +702,23 @@
   EXPECT_TRUE(camera_preview_widget);
 
   auto* preview_window = camera_preview_widget->GetNativeWindow();
-  const auto* overlay_container = preview_window->GetRootWindow()->GetChildById(
-      kShellWindowId_OverlayContainer);
+  const auto* menu_container = preview_window->GetRootWindow()->GetChildById(
+      kShellWindowId_MenuContainer);
   auto* parent = preview_window->parent();
-  // Parent of the preview should be the OverlayContainer when capture mode
+  // Parent of the preview should be the MenuContainer when capture mode
   // session is active with `kFullscreen` type. And the preview window should
   // be the top-most child of it.
-  EXPECT_EQ(parent, overlay_container);
-  EXPECT_EQ(overlay_container->children().back(), preview_window);
+  EXPECT_EQ(parent, menu_container);
+  EXPECT_EQ(menu_container->children().back(), preview_window);
 
   StartRecordingFromSource(CaptureModeSource::kFullscreen);
-  // Parent of the preview should be the OverlayContainer when video recording
+  // Parent of the preview should be the MenuContainer when video recording
   // in progress with `kFullscreen` type. And the preview window should be the
   // top-most child of it.
   preview_window = camera_preview_widget->GetNativeWindow();
   parent = preview_window->parent();
-  EXPECT_EQ(parent, overlay_container);
-  EXPECT_EQ(overlay_container->children().back(), preview_window);
+  EXPECT_EQ(parent, menu_container);
+  EXPECT_EQ(menu_container->children().back(), preview_window);
 }
 
 TEST_F(CaptureModeCameraTest, CameraPreviewWidgetStackingInRegion) {
@@ -725,13 +743,13 @@
   controller->SetUserCaptureRegion(gfx::Rect(10, 20, 80, 60),
                                    /*by_user=*/true);
   StartRecordingFromSource(CaptureModeSource::kRegion);
-  const auto* overlay_container = preview_window->GetRootWindow()->GetChildById(
-      kShellWindowId_OverlayContainer);
-  // Parent of the preview should be the OverlayContainer when video recording
+  const auto* menu_container = preview_window->GetRootWindow()->GetChildById(
+      kShellWindowId_MenuContainer);
+  // Parent of the preview should be the MenuContainer when video recording
   // in progress with `kRegion` type. And the preview window should be the
   // top-most child of it.
-  ASSERT_EQ(preview_window->parent(), overlay_container);
-  EXPECT_EQ(overlay_container->children().back(), preview_window);
+  ASSERT_EQ(preview_window->parent(), menu_container);
+  EXPECT_EQ(menu_container->children().back(), preview_window);
 }
 
 // Tests that camera preview widget is shown, hidden and parented correctly
@@ -755,10 +773,10 @@
   controller->SetUserCaptureRegion(capture_region, /*by_user=*/true);
 
   // After user capture region is set, parent of the preview should be the
-  // OverlayContainer.
-  const auto* overlay_container = preview_window->GetRootWindow()->GetChildById(
-      kShellWindowId_OverlayContainer);
-  ASSERT_EQ(preview_window->parent(), overlay_container);
+  // MenuContainer.
+  const auto* menu_container = preview_window->GetRootWindow()->GetChildById(
+      kShellWindowId_MenuContainer);
+  ASSERT_EQ(preview_window->parent(), menu_container);
 
   // Press the bottom right of selection region. Verify preview is hidden and
   // parent of the preview should be UnparentedContainer.
@@ -777,11 +795,11 @@
   EXPECT_EQ(preview_window->parent(), unparented_container);
 
   // Now release the drag to end selection region update. Verify preview is
-  // shown and parent of the preview should be OverlayContainer.
+  // shown and parent of the preview should be MenuContainer.
   event_generator->ReleaseLeftButton();
   EXPECT_FALSE(capture_session->is_drag_in_progress());
   EXPECT_TRUE(camera_preview_widget->IsVisible());
-  EXPECT_EQ(preview_window->parent(), overlay_container);
+  EXPECT_EQ(preview_window->parent(), menu_container);
 
   // Press in the selection region to move it around. Since in the
   // use case, selection region is not updated, preview should not be hidden.
@@ -789,18 +807,18 @@
   event_generator->set_current_screen_location(current_position);
   event_generator->PressLeftButton();
   EXPECT_TRUE(camera_preview_widget->IsVisible());
-  EXPECT_EQ(preview_window->parent(), overlay_container);
+  EXPECT_EQ(preview_window->parent(), menu_container);
 
   // Move mouse to move selection region around. Verify preview is shown.
   event_generator->MoveMouseTo(current_position + delta);
   EXPECT_TRUE(camera_preview_widget->IsVisible());
-  EXPECT_EQ(preview_window->parent(), overlay_container);
+  EXPECT_EQ(preview_window->parent(), menu_container);
 
   // Now release the move to end moving selection region. Verify preview is
   // shown.
   event_generator->ReleaseLeftButton();
   EXPECT_TRUE(camera_preview_widget->IsVisible());
-  EXPECT_EQ(preview_window->parent(), overlay_container);
+  EXPECT_EQ(preview_window->parent(), menu_container);
 }
 
 TEST_F(CaptureModeCameraTest, CameraPreviewWidgetStackingInWindow) {
@@ -1193,6 +1211,206 @@
   EXPECT_EQ(window(), capture_mode_session->GetSelectedWindow());
 }
 
+// Tests that capture label's opacity changes accordingly when it's overlapped
+// or it's not overlapped with camera preview. Also tests that when located
+// events is or is not on capture label, its opacity is updated accordingly.
+TEST_F(CaptureModeCameraTest,
+       CaptureLabelOpacityChangeWhenOverlappingWithCameraPreview) {
+  auto* controller =
+      StartCaptureSession(CaptureModeSource::kRegion, CaptureModeType::kVideo);
+  auto* capture_session = controller->capture_mode_session();
+  auto* camera_controller = GetCameraController();
+  AddDefaultCamera();
+  camera_controller->SetSelectedCamera(CameraId(kDefaultCameraModelId, 1));
+  const auto* camera_preview_widget =
+      camera_controller->camera_preview_widget();
+  const auto* capture_label_widget = capture_session->capture_label_widget();
+  const ui::Layer* capture_label_layer = capture_label_widget->GetLayer();
+
+  // Set capture region big enough to make capture label not overlapping with
+  // camera preview. Verify capture label is fully opaque.
+  const gfx::Rect capture_region(100, 100, 700, 700);
+  SelectCaptureRegion(capture_region);
+  EXPECT_FALSE(capture_label_widget->GetWindowBoundsInScreen().Intersects(
+      camera_preview_widget->GetWindowBoundsInScreen()));
+  EXPECT_EQ(capture_label_layer->GetTargetOpacity(), 1.f);
+
+  // Update capture region smaller to make capture label overlap with camera
+  // preview. Verify capture label is `kOverlapOpacity`.
+  const gfx::Vector2d delta(-500, -600);
+  auto* event_generator = GetEventGenerator();
+  event_generator->set_current_screen_location(capture_region.bottom_right());
+  event_generator->PressLeftButton();
+  event_generator->MoveMouseTo(capture_region.bottom_right() + delta);
+  event_generator->ReleaseLeftButton();
+  EXPECT_TRUE(capture_label_widget->GetWindowBoundsInScreen().Intersects(
+      camera_preview_widget->GetWindowBoundsInScreen()));
+  EXPECT_EQ(capture_label_layer->GetTargetOpacity(), kOverlapOpacity);
+
+  // Move mouse on top of capture label, verify it's updated to fully opaque
+  // even it's still overlapped with camera preview.
+  const gfx::Rect capture_lable_bounds =
+      capture_label_widget->GetWindowBoundsInScreen();
+  event_generator->MoveMouseTo(capture_lable_bounds.CenterPoint());
+  EXPECT_TRUE(capture_lable_bounds.Intersects(
+      camera_preview_widget->GetWindowBoundsInScreen()));
+  EXPECT_EQ(capture_label_layer->GetTargetOpacity(), 1.0f);
+
+  // Mouse mouse to the outside of capture label, verify it's updated to
+  // `kOverlapOpacity`.
+  const gfx::Vector2d delta1(50, 50);
+  event_generator->MoveMouseTo(capture_lable_bounds.bottom_right() + delta1);
+  EXPECT_EQ(capture_label_layer->GetTargetOpacity(), kOverlapOpacity);
+
+  // Click on the outside of the capture region to reset it, verify capture
+  // label is updated to full opaque.
+  const gfx::Rect current_capture_region = controller->user_capture_region();
+  event_generator->MoveMouseTo(current_capture_region.bottom_right() + delta1);
+  event_generator->ClickLeftButton();
+  EXPECT_EQ(capture_label_layer->GetTargetOpacity(), 1.0f);
+}
+
+TEST_F(CaptureModeCameraTest,
+       CaptureBarOpacityChangeWhenOverlappingWithCameraPreview) {
+  // Update display size and create a new window with customized size to make
+  // sure camera preview overlap with capture bar with capture source `kWindow`.
+  UpdateDisplay("1366x768");
+  std::unique_ptr<aura::Window> window(
+      CreateTestWindow(gfx::Rect(0, 195, 903, 492)));
+
+  auto* controller =
+      StartCaptureSession(CaptureModeSource::kWindow, CaptureModeType::kVideo);
+  auto* capture_session = controller->capture_mode_session();
+  auto* camera_controller = GetCameraController();
+  AddDefaultCamera();
+  camera_controller->SetSelectedCamera(CameraId(kDefaultCameraModelId, 1));
+  const auto* camera_preview_widget =
+      camera_controller->camera_preview_widget();
+  const auto* capture_bar_widget = capture_session->capture_mode_bar_widget();
+  const ui::Layer* capture_bar_layer = capture_bar_widget->GetLayer();
+
+  // Move mouse on top of `window` to set the selected window. Verify capture
+  // bar is `kOverlapOpacity`.
+  auto* event_generator = GetEventGenerator();
+  event_generator->MoveMouseTo(window->GetBoundsInScreen().CenterPoint());
+  EXPECT_EQ(capture_session->GetSelectedWindow(), window.get());
+  EXPECT_TRUE(capture_bar_widget->GetWindowBoundsInScreen().Intersects(
+      camera_preview_widget->GetWindowBoundsInScreen()));
+  EXPECT_EQ(capture_bar_layer->GetTargetOpacity(), kOverlapOpacity);
+
+  // Move mouse on top of capture bar. Verify capture bar is updated to fully
+  // opaque.
+  event_generator->MoveMouseTo(
+      capture_bar_widget->GetWindowBoundsInScreen().CenterPoint());
+  EXPECT_TRUE(capture_bar_widget->GetWindowBoundsInScreen().Intersects(
+      camera_preview_widget->GetWindowBoundsInScreen()));
+  EXPECT_EQ(capture_bar_layer->GetTargetOpacity(), 1.0f);
+
+  // Mouse mouse to the outside of capture bar, verify it's updated to
+  // `kOverlapOpacity`.
+  const gfx::Point capture_bar_origin =
+      capture_bar_widget->GetWindowBoundsInScreen().origin();
+  event_generator->MoveMouseTo(capture_bar_origin.x() - 10,
+                               capture_bar_origin.y() - 10);
+  EXPECT_EQ(capture_bar_layer->GetTargetOpacity(), kOverlapOpacity);
+}
+
+TEST_F(CaptureModeCameraTest, CaptureBarOpacityChangeOnDisplayRotation) {
+  // Update display size and create a new window with customized size to make
+  // sure camera preview overlap with capture bar with capture source `kWindow`.
+  UpdateDisplay("1366x768");
+  std::unique_ptr<aura::Window> window(
+      CreateTestWindow(gfx::Rect(0, 195, 903, 492)));
+
+  auto* controller =
+      StartCaptureSession(CaptureModeSource::kWindow, CaptureModeType::kVideo);
+  auto* capture_session = controller->capture_mode_session();
+  auto* camera_controller = GetCameraController();
+  AddDefaultCamera();
+  camera_controller->SetSelectedCamera(CameraId(kDefaultCameraModelId, 1));
+  const auto* camera_preview_widget =
+      camera_controller->camera_preview_widget();
+  const auto* capture_bar_widget = capture_session->capture_mode_bar_widget();
+  const ui::Layer* capture_bar_layer = capture_bar_widget->GetLayer();
+
+  // Move mouse on top of `window` to set the selected window. Verify capture
+  // bar is `kOverlapOpacity`.
+  auto* event_generator = GetEventGenerator();
+  event_generator->MoveMouseTo(window->GetBoundsInScreen().CenterPoint());
+  EXPECT_EQ(capture_session->GetSelectedWindow(), window.get());
+  EXPECT_TRUE(capture_bar_widget->GetWindowBoundsInScreen().Intersects(
+      camera_preview_widget->GetWindowBoundsInScreen()));
+  EXPECT_EQ(capture_bar_layer->GetTargetOpacity(), kOverlapOpacity);
+
+  // Rotate the primary display by 90 degrees. Verify that capture bar no longer
+  // overlaps with camera preview and it's updated to fully opaque.
+  Shell::Get()->display_manager()->SetDisplayRotation(
+      WindowTreeHostManager::GetPrimaryDisplayId(), display::Display::ROTATE_90,
+      display::Display::RotationSource::USER);
+  EXPECT_FALSE(capture_bar_widget->GetWindowBoundsInScreen().Intersects(
+      camera_preview_widget->GetWindowBoundsInScreen()));
+  EXPECT_EQ(capture_bar_layer->GetTargetOpacity(), 1.0f);
+
+  // Rotate the primary display by 180 degrees. Verify that capture bar is
+  // overlapped with camera preview and it's updated to `kOverlapOpacity`.
+  Shell::Get()->display_manager()->SetDisplayRotation(
+      WindowTreeHostManager::GetPrimaryDisplayId(),
+      display::Display::ROTATE_180, display::Display::RotationSource::USER);
+  EXPECT_TRUE(capture_bar_widget->GetWindowBoundsInScreen().Intersects(
+      camera_preview_widget->GetWindowBoundsInScreen()));
+  EXPECT_EQ(capture_bar_layer->GetTargetOpacity(), kOverlapOpacity);
+}
+
+TEST_F(CaptureModeCameraTest, CaptureLabelOpacityChangeOnCaptureSourceChange) {
+  auto* controller =
+      StartCaptureSession(CaptureModeSource::kRegion, CaptureModeType::kVideo);
+  auto* capture_session = controller->capture_mode_session();
+  auto* camera_controller = GetCameraController();
+  AddDefaultCamera();
+  camera_controller->SetSelectedCamera(CameraId(kDefaultCameraModelId, 1));
+  auto* camera_preview_widget = camera_controller->camera_preview_widget();
+  auto* capture_label_widget = capture_session->capture_label_widget();
+  ui::Layer* capture_label_layer = capture_label_widget->GetLayer();
+
+  // Select capture region to make sure capture label is overlapped with
+  // camera preview. Verify capture label is `kOverlapOpacity`.
+  SelectCaptureRegion({100, 100, 200, 100});
+  EXPECT_TRUE(capture_label_widget->GetWindowBoundsInScreen().Intersects(
+      camera_preview_widget->GetWindowBoundsInScreen()));
+  EXPECT_EQ(capture_label_layer->GetTargetOpacity(), kOverlapOpacity);
+
+  // Change the capture source from `kRegion` to `kFullscreen`, verify capture
+  // label is updated to fully opaque.
+  controller->SetSource(CaptureModeSource::kFullscreen);
+  EXPECT_EQ(capture_label_layer->GetTargetOpacity(), 1.0f);
+}
+
+TEST_F(CaptureModeCameraTest,
+       CaptureLabelOpacityChangeWhileVideoRecordingInProgress) {
+  auto* controller =
+      StartCaptureSession(CaptureModeSource::kRegion, CaptureModeType::kVideo);
+  auto* camera_controller = GetCameraController();
+  AddDefaultCamera();
+  camera_controller->SetSelectedCamera(CameraId(kDefaultCameraModelId, 1));
+  auto* camera_preview_widget = camera_controller->camera_preview_widget();
+  controller->SetUserCaptureRegion({100, 100, 200, 100}, /*by_user=*/true);
+
+  StartVideoRecordingImmediately();
+  EXPECT_FALSE(controller->IsActive());
+
+  // Start a new capture session, verify even capture label is overlapped with
+  // camera preview, it's still fully opaque since camera preview does not
+  // belong to the new capture session.
+  controller->Start(CaptureModeEntryType::kQuickSettings);
+  EXPECT_EQ(CaptureModeSource::kRegion, controller->source());
+  auto* capture_session = controller->capture_mode_session();
+
+  const auto* capture_label_widget = capture_session->capture_label_widget();
+  EXPECT_TRUE(capture_label_widget->GetWindowBoundsInScreen().Intersects(
+      camera_preview_widget->GetWindowBoundsInScreen()));
+  EXPECT_EQ(capture_label_widget->GetLayer()->GetTargetOpacity(), 1.0f);
+}
+
 class CaptureModeCameraPreviewTest
     : public CaptureModeCameraTest,
       public testing::WithParamInterface<CaptureModeSource> {
@@ -1274,8 +1492,8 @@
   // capture bounds.
   VerifyPreviewAlignment(GetCaptureBoundsInScreen());
 
-  // Rotate the primary display by 90 degrees. Verify that the camera preview is
-  // still at the bottom right corner of capture bounds.
+  // Rotate the primary display by 90 degrees. Verify that the camera preview
+  // is still at the bottom right corner of capture bounds.
   Shell::Get()->display_manager()->SetDisplayRotation(
       WindowTreeHostManager::GetPrimaryDisplayId(), display::Display::ROTATE_90,
       display::Display::RotationSource::USER);
@@ -1302,9 +1520,9 @@
 }
 
 // Tests that when camera preview is being dragged, at the end of the drag, it
-// should be snapped to the correct snap position. It tests two use cases, when
-// capture session is active and when there's a video recording in progress
-// including drag to snap by mouse and by touch.
+// should be snapped to the correct snap position. It tests two use cases,
+// when capture session is active and when there's a video recording in
+// progress including drag to snap by mouse and by touch.
 TEST_P(CaptureModeCameraPreviewTest, CameraPreviewDragToSnap) {
   StartCaptureSessionWithParam();
   auto* camera_controller = GetCameraController();
@@ -1321,8 +1539,8 @@
   VerifyPreviewAlignment(GetCaptureBoundsInScreen());
 
   // Drag and drop camera preview by mouse to the top right of the
-  // `capture_bounds_center_point`, verify that camera preview is snapped to the
-  // top right with correct position.
+  // `capture_bounds_center_point`, verify that camera preview is snapped to
+  // the top right with correct position.
   DragPreviewToPoint(preview_widget, {capture_bounds_center_point.x() + 20,
                                       capture_bounds_center_point.y() - 20});
   EXPECT_EQ(CameraPreviewSnapPosition::kTopRight,
@@ -1340,8 +1558,9 @@
             camera_controller->camera_preview_snap_position());
   VerifyPreviewAlignment(GetCaptureBoundsInScreen());
 
-  // Start video recording, verify camera preview is snapped to the correct snap
-  // position at the end of drag when there's a video recording in progress.
+  // Start video recording, verify camera preview is snapped to the correct
+  // snap position at the end of drag when there's a video recording in
+  // progress.
   StartVideoRecordingImmediately();
   EXPECT_FALSE(CaptureModeController::Get()->IsActive());
 
@@ -1354,9 +1573,9 @@
             camera_controller->camera_preview_snap_position());
   VerifyPreviewAlignment(GetCaptureBoundsInScreen());
 
-  // Now drag and drop camera preview by touch to the bottom right of the center
-  // point, verify that camera preview is snapped to the bottom right with
-  // correct position.
+  // Now drag and drop camera preview by touch to the bottom right of the
+  // center point, verify that camera preview is snapped to the bottom right
+  // with correct position.
   DragPreviewToPoint(preview_widget,
                      {capture_bounds_center_point.x() + 20,
                       capture_bounds_center_point.y() + 20},
@@ -1366,6 +1585,78 @@
   VerifyPreviewAlignment(GetCaptureBoundsInScreen());
 }
 
+// Tests the use case after pressing on the resize button on camera preview and
+// releasing the press outside of camera preview, camera preview is still
+// draggable. Regression test for https://crbug.com/1308885.
+TEST_P(CaptureModeCameraPreviewTest,
+       CameraPreviewDragToSnapAfterPressOnResizeButton) {
+  StartCaptureSessionWithParam();
+  auto* camera_controller = GetCameraController();
+  AddDefaultCamera();
+  camera_controller->SetSelectedCamera(CameraId(kDefaultCameraModelId, 1));
+  auto* preview_widget = camera_controller->camera_preview_widget();
+  auto* resize_button = GetPreviewResizeButton();
+  const int camera_previw_width =
+      preview_widget->GetWindowBoundsInScreen().width();
+  const gfx::Point capture_bounds_center_point =
+      GetCaptureBoundsInScreen().CenterPoint();
+  const gfx::Point center_point_of_resize_button =
+      resize_button->GetBoundsInScreen().CenterPoint();
+
+  // By default the snap position of preview widget should be `kBottomRight`.
+  EXPECT_EQ(CameraPreviewSnapPosition::kBottomRight,
+            camera_controller->camera_preview_snap_position());
+
+  auto* event_generator = GetEventGenerator();
+  event_generator->set_current_screen_location(center_point_of_resize_button);
+  event_generator->PressLeftButton();
+
+  const gfx::Vector2d delta(-camera_previw_width, -camera_previw_width);
+  // Now move mouse to the outside of the camera preview and then release.
+  event_generator->MoveMouseTo(center_point_of_resize_button + delta);
+  event_generator->ReleaseLeftButton();
+
+  // Now try to drag the camera preview to the top left, after camera preview is
+  // snapped, the current snap position should be `kTopLeft`.
+  DragPreviewToPoint(preview_widget, capture_bounds_center_point + delta);
+  EXPECT_EQ(CameraPreviewSnapPosition::kTopLeft,
+            camera_controller->camera_preview_snap_position());
+}
+
+TEST_P(CaptureModeCameraPreviewTest, CaptureUisVisibilityChangeOnDragAndDrop) {
+  StartCaptureSessionWithParam();
+  auto* camera_controller = GetCameraController();
+  auto* capture_session = CaptureModeController::Get()->capture_mode_session();
+  AddDefaultCamera();
+  camera_controller->SetSelectedCamera(CameraId(kDefaultCameraModelId, 1));
+  auto* preview_widget = camera_controller->camera_preview_widget();
+  const gfx::Point center_point_of_preview_widget =
+      preview_widget->GetWindowBoundsInScreen().CenterPoint();
+
+  const auto* capture_bar_widget = capture_session->capture_mode_bar_widget();
+  const auto* capture_label_widget = capture_session->capture_label_widget();
+
+  // Press on top of the preview widget. Verify capture bar and capture label
+  // are hidden.
+  auto* event_generator = GetEventGenerator();
+  event_generator->set_current_screen_location(center_point_of_preview_widget);
+  event_generator->PressLeftButton();
+  EXPECT_FALSE(capture_bar_widget->IsVisible());
+  EXPECT_FALSE(capture_label_widget->IsVisible());
+
+  // Now drag and move the preview widget. Verify capture bar and capture
+  // label are still hidden.
+  const gfx::Vector2d delta(-50, -60);
+  event_generator->MoveMouseTo(center_point_of_preview_widget + delta);
+  EXPECT_FALSE(capture_bar_widget->IsVisible());
+  EXPECT_FALSE(capture_label_widget->IsVisible());
+
+  // Release the drag. Verify capture bar and capture label are shown again.
+  event_generator->ReleaseLeftButton();
+  EXPECT_TRUE(capture_bar_widget->IsVisible());
+  EXPECT_TRUE(capture_label_widget->IsVisible());
+}
+
 TEST_P(CaptureModeCameraPreviewTest, CameraPreviewDragToSnapOnMultipleDisplay) {
   UpdateDisplay("800x700,801+0-800x700");
 
@@ -1383,8 +1674,8 @@
       GetCaptureBoundsInScreen().CenterPoint();
 
   // Drag and drop camera preview by mouse to the top right of the
-  // `capture_bounds_center_point`, verify that camera preview is snapped to the
-  // top right with correct position.
+  // `capture_bounds_center_point`, verify that camera preview is snapped to
+  // the top right with correct position.
   DragPreviewToPoint(preview_widget, {capture_bounds_center_point.x() + 20,
                                       capture_bounds_center_point.y() - 20});
   EXPECT_EQ(CameraPreviewSnapPosition::kTopRight,
@@ -1427,9 +1718,9 @@
   EXPECT_EQ(preview_widget->GetWindowBoundsInScreen(),
             preview_bounds_in_screen_before_drag);
 
-  // Try to drag and drop camera preview by touch to the top left of the current
-  // capture bounds' center point, verity it's not moved. Also verify the snap
-  // position is not updated.
+  // Try to drag and drop camera preview by touch to the top left of the
+  // current capture bounds' center point, verity it's not moved. Also verify
+  // the snap position is not updated.
   DragPreviewToPoint(preview_widget,
                      {capture_bounds_center_point.x() - 20,
                       capture_bounds_center_point.y() - 20},
@@ -1477,9 +1768,9 @@
                      /*drop=*/false);
   EXPECT_EQ(cursor_manager->GetCursor(), ui::mojom::CursorType::kPointer);
 
-  // Continue dragging and then drop camera preview, make sure cursor's position
-  // is outside of camera preview after it's snapped. Verify cursor type is
-  // updated to the correct type of the current capture source.
+  // Continue dragging and then drop camera preview, make sure cursor's
+  // position is outside of camera preview after it's snapped. Verify cursor
+  // type is updated to the correct type of the current capture source.
   DragPreviewToPoint(preview_widget, {camera_preview_origin_point.x() - 20,
                                       camera_preview_origin_point.y() - 20});
   EXPECT_EQ(cursor_manager->GetCursor(), GetCursorTypeOnCaptureSurface());
diff --git a/ash/capture_mode/capture_mode_session.cc b/ash/capture_mode/capture_mode_session.cc
index 8ea5ef5..12653e89 100644
--- a/ash/capture_mode/capture_mode_session.cc
+++ b/ash/capture_mode/capture_mode_session.cc
@@ -47,6 +47,7 @@
 #include "ui/aura/env.h"
 #include "ui/aura/window.h"
 #include "ui/aura/window_delegate.h"
+#include "ui/aura/window_observer.h"
 #include "ui/aura/window_tracker.h"
 #include "ui/base/cursor/cursor_factory.h"
 #include "ui/base/l10n/l10n_util.h"
@@ -68,9 +69,11 @@
 #include "ui/gfx/scoped_canvas.h"
 #include "ui/gfx/shadow_value.h"
 #include "ui/gfx/skia_paint_util.h"
+#include "ui/views/animation/animation_builder.h"
 #include "ui/views/background.h"
 #include "ui/views/controls/button/label_button.h"
 #include "ui/views/controls/label.h"
+#include "ui/views/widget/widget.h"
 #include "ui/wm/core/coordinate_conversion.h"
 
 namespace ash {
@@ -166,17 +169,20 @@
 // widget will scale up from 80% -> 100%.
 constexpr float kLabelScaleDownOnPhaseChange = 0.8;
 
-// Animation parameters for capture bar overlapping the user capture region.
-// The default animation duration for opacity changes to the capture bar.
-constexpr base::TimeDelta kCaptureBarOpacityChangeDuration =
+// Animation parameters for capture UI (capture bar, capture label) overlapping
+// the user capture region or camera preview. The default animation duration for
+// opacity changes to the capture UI.
+constexpr base::TimeDelta kCaptureUIOpacityChangeDuration =
     base::Milliseconds(100);
 // The animation duration for showing the capture bar on mouse/touch release.
 constexpr base::TimeDelta kCaptureBarOnReleaseOpacityChangeDuration =
     base::Milliseconds(167);
-// When the capture bar and user capture region overlap and the mouse is not
-// hovering over the capture bar, drop the opacity to this value to make the
-// region easier to see.
-constexpr float kCaptureBarOverlapOpacity = 0.1;
+
+// When capture UI (capture bar, capture label) is overlapped with user
+// capture region or camera preview, and the mouse is not hovering over the
+// capture UI, drop the opacity to this value to make the region or camera
+// preview easier to see.
+constexpr float kCaptureUiOverlapOpacity = 0.1;
 
 // If the user is using keyboard only and they are on the selecting region
 // phase, they can create default region which is centered and sized to this
@@ -380,6 +386,41 @@
   return false;
 }
 
+views::Widget* GetCameraPreviewWidget() {
+  auto* camera_controller = CaptureModeController::Get()->camera_controller();
+  return camera_controller ? camera_controller->camera_preview_widget()
+                           : nullptr;
+}
+
+// Returns true if the given `event` is targeted on the camera preview.
+// Otherwise, returns false.
+bool IsEventTargetedOnCameraPreview(ui::LocatedEvent* event) {
+  auto* camera_preview_widget = GetCameraPreviewWidget();
+  if (camera_preview_widget && camera_preview_widget->IsVisible()) {
+    auto* target = static_cast<aura::Window*>(event->target());
+    if (camera_preview_widget->GetNativeWindow()->Contains(target))
+      return true;
+  }
+  return false;
+}
+
+// Returns true if the given `widget` intersects with the camera preview.
+// Otherwise, returns false;
+bool IsWidgetOverlappedWithCameraPreview(views::Widget* widget) {
+  // Return false immediately if there's a video recording in propress since
+  // the camera preview doesn't belong to the current capture session.
+  if (CaptureModeController::Get()->is_recording_in_progress())
+    return false;
+
+  auto* camera_preview_widget = GetCameraPreviewWidget();
+  if (!camera_preview_widget)
+    return false;
+
+  return camera_preview_widget->IsVisible() &&
+         camera_preview_widget->GetWindowBoundsInScreen().Intersects(
+             widget->GetWindowBoundsInScreen());
+}
+
 }  // namespace
 
 // -----------------------------------------------------------------------------
@@ -526,6 +567,50 @@
 };
 
 // -----------------------------------------------------------------------------
+// CaptureModeSession::ParentContainerObserver:
+
+// The observer class to observer window added to or removed from the parent
+// container `kShellWindowId_MenuContainer`. Capture UIs (capture bar, capture
+// label, capture settings, camera preview) are all parented to the parent
+// container, thus whenever there's a window added or removed, we need to call
+// `RefreshStackingOrder` to ensure the stacking order is correct for them.
+class CaptureModeSession::ParentContainerObserver
+    : public aura::WindowObserver {
+ public:
+  ParentContainerObserver(aura::Window* parent_container,
+                          CaptureModeSession* capture_mode_session)
+      : parent_container_(parent_container),
+        capture_mode_session_(capture_mode_session) {
+    parent_container_->AddObserver(this);
+  }
+
+  ParentContainerObserver(const CursorSetter&) = delete;
+  ParentContainerObserver& operator=(const CursorSetter&) = delete;
+
+  ~ParentContainerObserver() override {
+    parent_container_->RemoveObserver(this);
+  }
+
+  // aura::WindowObserver:
+  void OnWindowAdded(aura::Window* window) override {
+    capture_mode_session_->RefreshStackingOrder();
+    capture_mode_session_->MaybeUpdateCaptureUisOpacity();
+  }
+
+  void OnWindowRemoved(aura::Window* window) override {
+    capture_mode_session_->RefreshStackingOrder();
+    capture_mode_session_->MaybeUpdateCaptureUisOpacity();
+  }
+
+ private:
+  aura::Window* const parent_container_;
+
+  // Pointer to current capture session. Not nullptr during this lifecycle.
+  // Capture session owns `this`.
+  CaptureModeSession* const capture_mode_session_;
+};
+
+// -----------------------------------------------------------------------------
 // CaptureModeSession:
 
 CaptureModeSession::CaptureModeSession(CaptureModeController* controller,
@@ -584,6 +669,8 @@
   layer()->SetFillsBoundsOpaquely(false);
   layer()->set_delegate(this);
   auto* parent = GetParentContainer(current_root_);
+  parent_container_observer_ =
+      std::make_unique<ParentContainerObserver>(parent, this);
   parent->layer()->Add(layer());
   layer()->SetBounds(parent->bounds());
 
@@ -607,7 +694,6 @@
     focus_cycler_->AdvanceFocus(/*reverse=*/false);
 
   UpdateCaptureLabelWidget(CaptureLabelAnimation::kNone);
-  RefreshStackingOrder(parent);
 
   UpdateCursor(display::Screen::GetScreen()->GetCursorScreenPoint(),
                /*is_touch=*/false);
@@ -869,20 +955,20 @@
 aura::Window* CaptureModeSession::GetCameraPreviewParentWindow() const {
   auto* controller = CaptureModeController::Get();
   DCHECK(!controller->is_recording_in_progress());
-  auto* overlay_container =
-      current_root_->GetChildById(kShellWindowId_OverlayContainer);
+  auto* menu_container =
+      current_root_->GetChildById(kShellWindowId_MenuContainer);
   auto* unparented_container =
       current_root_->GetChildById(kShellWindowId_UnparentedContainer);
 
   switch (controller->source()) {
     case CaptureModeSource::kFullscreen:
-      return overlay_container;
+      return menu_container;
     case CaptureModeSource::kRegion:
       return controller_->user_capture_region().IsEmpty() ||
                      (is_drag_in_progress_ &&
                       fine_tune_position_ != FineTunePosition::kCenter)
                  ? unparented_container
-                 : overlay_container;
+                 : menu_container;
     case CaptureModeSource::kWindow:
       aura::Window* selected_window = GetSelectedWindow();
       return selected_window ? selected_window : unparented_container;
@@ -914,8 +1000,14 @@
 }
 
 void CaptureModeSession::OnPaintLayer(const ui::PaintContext& context) {
-  if (!is_all_uis_visible_)
+  // If the drag of camera preview is in progress, we will hide other capture
+  // UIs (capture bar, capture label), but we should still paint the layer to
+  // indicate the capture surface where user can drag camera preview on.
+  if (!is_all_uis_visible_ &&
+      !(controller_->camera_controller() &&
+        controller_->camera_controller()->is_drag_in_progress())) {
     return;
+  }
 
   ui::PaintRecorder recorder(context, layer()->size());
 
@@ -1092,6 +1184,7 @@
   if (capture_label_widget_)
     UpdateCaptureLabelWidget(CaptureLabelAnimation::kNone);
   layer()->SchedulePaint(layer()->bounds());
+  MaybeUpdateCaptureUisOpacity();
 }
 
 void CaptureModeSession::OnFolderSelected(const base::FilePath& path) {
@@ -1225,6 +1318,99 @@
       capture_mode_bar_view_, capture_mode_settings_view_));
 }
 
+void CaptureModeSession::MaybeUpdateCaptureUisOpacity(
+    absl::optional<gfx::Point> cursor_screen_location) {
+  if (is_shutting_down_)
+    return;
+
+  // TODO(conniekxu): Handle this for tablet mode which doesn't have a cursor
+  // screen point.
+  if (!cursor_screen_location) {
+    cursor_screen_location =
+        display::Screen::GetScreen()->GetCursorScreenPoint();
+  }
+
+  base::flat_map<views::Widget*, /*opacity=*/float> widget_opacity_map;
+  if (capture_mode_bar_widget_)
+    widget_opacity_map[capture_mode_bar_widget_.get()] = 1.f;
+  if (capture_label_widget_)
+    widget_opacity_map[capture_label_widget_.get()] = 1.f;
+
+  const bool is_settings_visible = capture_mode_settings_widget_ &&
+                                   capture_mode_settings_widget_->IsVisible();
+
+  for (auto& pair : widget_opacity_map) {
+    views::Widget* widget = pair.first;
+    float& opacity = pair.second;
+    DCHECK(widget->GetLayer());
+
+    if (widget->GetWindowBoundsInScreen().Contains(*cursor_screen_location)) {
+      continue;
+    }
+
+    if (widget == capture_mode_bar_widget_.get()) {
+      // If capture setting is visible, capture bar should be fully opaque even
+      // if it's overlapped with camera preview.
+      if (is_settings_visible)
+        continue;
+
+      // If drag for capture region is in progress, capture bar should be
+      // hidden.
+      if (is_drag_in_progress_) {
+        opacity = 0.f;
+        continue;
+      }
+    }
+
+    if (IsWidgetOverlappedWithCameraPreview(widget))
+      opacity = kCaptureUiOverlapOpacity;
+  }
+
+  for (const auto& pair : widget_opacity_map) {
+    ui::Layer* layer = pair.first->GetLayer();
+    const float& opacity = pair.second;
+    if (layer->GetTargetOpacity() == opacity)
+      continue;
+
+    views::AnimationBuilder()
+        .SetPreemptionStrategy(
+            ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET)
+        .Once()
+        .SetDuration(kCaptureUIOpacityChangeDuration)
+        .SetOpacity(layer, opacity, gfx::Tween::FAST_OUT_SLOW_IN);
+  }
+}
+
+void CaptureModeSession::OnCameraPreviewDragStarted() {
+  DCHECK(!controller_->is_recording_in_progress());
+
+  // If settings menu is shown at the beginning of drag, we should close it.
+  if (capture_mode_settings_widget_)
+    SetSettingsMenuShown(false);
+
+  // Hide capture UIs while dragging camera preview.
+  HideAllUis();
+}
+
+void CaptureModeSession::OnCameraPreviewDragEnded(
+    const gfx::Point& screen_location,
+    bool is_touch) {
+  // If CaptureUIs (capture bar, capture label) are overlapped with camera
+  // preview and cursor is not on top of it, its opacity should be updated to
+  // `kCaptureUiOverlapOpacity` instead of fully opaque.
+  MaybeUpdateCaptureUisOpacity(screen_location);
+
+  // Show capture UIs which are hidden in `OnCameraPreviewDragStarted`.
+  ShowAllUis();
+
+  // Make sure cursor is updated correctly after camera preview is snapped.
+  UpdateCursor(screen_location, is_touch);
+}
+
+void CaptureModeSession::OnCameraPreviewBoundsChanged() {
+  MaybeUpdateCaptureUisOpacity();
+}
+
 std::vector<views::Widget*> CaptureModeSession::GetAvailableWidgets() {
   std::vector<views::Widget*> result;
   DCHECK(capture_mode_bar_widget_);
@@ -1248,6 +1434,7 @@
     // without animation) when ShowAllUis() is called.
     widget->GetNativeWindow()->SetProperty(aura::client::kAnimationsDisabledKey,
                                            true);
+
     // The layer's opacity could be less than 1.f if the widget was hidden
     // before we disabled the animations above. We need to reset the opacity
     // back to 1.f as we will hide the widget without animation.
@@ -1328,15 +1515,35 @@
   return window ? window->bounds() : gfx::Rect();
 }
 
-void CaptureModeSession::RefreshStackingOrder(aura::Window* parent_container) {
+void CaptureModeSession::RefreshStackingOrder() {
+  if (is_shutting_down_)
+    return;
+
+  auto* parent_container = GetParentContainer(current_root_);
   DCHECK(parent_container);
-  auto* capture_mode_bar_layer = capture_mode_bar_widget_->GetLayer();
   auto* overlay_layer = layer();
   auto* parent_container_layer = parent_container->layer();
-
   parent_container_layer->StackAtTop(overlay_layer);
-  parent_container_layer->StackAtTop(capture_label_widget_->GetLayer());
-  parent_container_layer->StackAtTop(capture_mode_bar_layer);
+
+  std::vector<views::Widget*> widget_in_order;
+
+  auto* camera_preview_widget = GetCameraPreviewWidget();
+  // We don't need to update the stacking order for camera preview if
+  // there's a video recording in progress, since the camera preview don't
+  // belong to the current capture session.
+  if (camera_preview_widget && !controller_->is_recording_in_progress())
+    widget_in_order.emplace_back(camera_preview_widget);
+  if (capture_label_widget_)
+    widget_in_order.emplace_back(capture_label_widget_.get());
+  if (capture_mode_bar_widget_)
+    widget_in_order.emplace_back(capture_mode_bar_widget_.get());
+  if (capture_mode_settings_widget_)
+    widget_in_order.emplace_back(capture_mode_settings_widget_.get());
+
+  for (auto* widget : widget_in_order) {
+    if (widget->GetNativeWindow()->parent() == parent_container)
+      parent_container_layer->StackAtTop(widget->GetLayer());
+  }
 }
 
 void CaptureModeSession::PaintCaptureRegion(gfx::Canvas* canvas) {
@@ -1522,20 +1729,36 @@
     RefreshBarWidgetBounds();
   }
 
+  MaybeUpdateCaptureUisOpacity(screen_location);
+
   if (IsDragAllowedOnCameraPreview(screen_location)) {
+    DCHECK(!controller_->is_recording_in_progress());
     // Update cursor type when the event is on top of camera preview.
     UpdateCursor(screen_location, is_touch);
+
     // Pass the event to camera preview to handle it if the event is on top of
     // camera preview and there's no video recording is in progress.
     return;
   }
 
-  const bool is_event_on_settings_menu =
-      IsEventInSettingsMenuBounds(screen_location);
+  // If the event is targeted on the camera preview, even it's not located
+  // on the camera preview, we should still pass the event to camera preview
+  // to handle it. For example, when pressing on the resize button inside camera
+  // preview, but release the press outside of camera preview, even the release
+  // event is not on the camera preview, we should still pass the event to it,
+  // otherwise camera preview will wait for the release event forever which will
+  // make the regular drag for camera preview not work.
+  if (!controller_->is_recording_in_progress() &&
+      IsEventTargetedOnCameraPreview(event)) {
+    UpdateCursor(screen_location, is_touch);
+    return;
+  }
 
   const bool is_event_on_capture_bar =
       capture_mode_bar_widget_->GetWindowBoundsInScreen().Contains(
           screen_location);
+  const bool is_event_on_settings_menu =
+      IsEventInSettingsMenuBounds(screen_location);
   const bool is_event_on_capture_bar_or_menu =
       is_event_on_capture_bar || is_event_on_settings_menu;
   const bool is_event_on_settings_button =
@@ -1671,8 +1894,10 @@
         if (capture_mode_settings_widget_ && !is_event_on_capture_bar_or_menu)
           SetSettingsMenuShown(/*shown=*/false);
 
+        // TODO(crbug.com/1310310): Consider combining
+        // `UpdateCaptureBarWidgetOpacity` into `MaybeUpdateCaptureUisOpacity`.
         UpdateCaptureBarWidgetOpacity(
-            is_event_on_capture_bar_or_menu ? 1.f : kCaptureBarOverlapOpacity,
+            is_event_on_capture_bar_or_menu ? 1.f : kCaptureUiOverlapOpacity,
             /*on_release=*/false);
       }
       break;
@@ -1841,10 +2066,19 @@
   // Do a repaint to show the affordance circles.
   RepaintRegion();
 
-  // Show the camera which may have been hidden in `OnLocatedEventPressed`
-  // regardless of whether we're selecting a region for the first time, or just
-  // dragging to fine tune it.
-  MaybeReparentCameraPreviewWidget();
+  // Run `MaybeReparentCameraPreviewWidget` when user releases the drag at
+  // the exit of this function's scope to show the camera preview which may have
+  // been hidden in `OnLocatedEventPressed`. Please notice, we should call
+  // `MaybeReparentCameraPreviewWidget` no matter if the event is on the capture
+  // bar or not, since at the end of drag, the event may happen to be located on
+  // the capture bar, we should still show the camera preview at this usecase.
+  // The reason we want to run it at the exit of this function is if
+  // `is_selecting_region_` is true, we want to wait until the capture label is
+  // updated since capture label's opacity may need to be updated based on if
+  // it's overlapped with camera preview or not.
+  base::ScopedClosureRunner deferred_runner(
+      base::BindOnce(&CaptureModeSession::MaybeReparentCameraPreviewWidget,
+                     weak_ptr_factory_.GetWeakPtr()));
 
   if (!is_selecting_region_)
     return;
@@ -2084,6 +2318,8 @@
         gfx::GetScaleTransform(gfx::Point(center_point.x() - bounds.x(),
                                           center_point.y() - bounds.y()),
                                kLabelScaleUpOnCountdown));
+    // TODO (crbug/1310310): Consider combining the following codes into
+    // `MaybeUpdateCaptureUisOpacity`.
     layer->SetOpacity(0.f);
 
     // Fade in.
@@ -2252,6 +2488,9 @@
   new_root->AddObserver(this);
 
   auto* new_parent = GetParentContainer(new_root);
+  parent_container_observer_ =
+      std::make_unique<ParentContainerObserver>(new_parent, this);
+
   new_parent->layer()->Add(layer());
   layer()->SetBounds(new_parent->bounds());
 
@@ -2314,7 +2553,7 @@
       capture_bar_layer->GetAnimator());
   capture_bar_settings.SetTransitionDuration(
       on_release ? kCaptureBarOnReleaseOpacityChangeDuration
-                 : kCaptureBarOpacityChangeDuration);
+                 : kCaptureUIOpacityChangeDuration);
   capture_bar_settings.SetTweenType(on_release ? gfx::Tween::FAST_OUT_SLOW_IN
                                                : gfx::Tween::LINEAR);
   capture_bar_settings.SetPreemptionStrategy(
@@ -2340,7 +2579,7 @@
   // TODO(richui): Update this for tablet mode.
   UpdateCaptureBarWidgetOpacity(
       region_intersects_capture_bar && !is_event_on_capture_bar_or_menu
-          ? kCaptureBarOverlapOpacity
+          ? kCaptureUiOverlapOpacity
           : 1.f,
       /*on_release=*/true);
 
diff --git a/ash/capture_mode/capture_mode_session.h b/ash/capture_mode/capture_mode_session.h
index 699b61a61..d38edb5 100644
--- a/ash/capture_mode/capture_mode_session.h
+++ b/ash/capture_mode/capture_mode_session.h
@@ -23,6 +23,7 @@
 #include "ui/display/display_observer.h"
 #include "ui/events/event.h"
 #include "ui/events/event_handler.h"
+#include "ui/gfx/geometry/point.h"
 #include "ui/views/controls/button/button.h"
 #include "ui/views/widget/unique_widget_ptr.h"
 #include "ui/views/widget/widget.h"
@@ -195,12 +196,25 @@
   // updated correspondingly.
   void MaybeUpdateSettingsBounds();
 
+  // Called when opacity of capture UIs (capture bar, capture label) may need to
+  // be updated. For example, when camera preview is created, destroyed,
+  // reparented, display metrics change or located events enter / exit / move
+  // on capture UI.
+  void MaybeUpdateCaptureUisOpacity(
+      absl::optional<gfx::Point> cursor_screen_location = absl::nullopt);
+
+  void OnCameraPreviewDragStarted();
+  void OnCameraPreviewDragEnded(const gfx::Point& screen_location,
+                                bool is_touch);
+  void OnCameraPreviewBoundsChanged();
+
  private:
   friend class CaptureModeSettingsTestApi;
   friend class CaptureModeSessionFocusCycler;
   friend class CaptureModeSessionTestApi;
   friend class CaptureModeTestApi;
   class CursorSetter;
+  class ParentContainerObserver;
 
   enum class CaptureLabelAnimation {
     // No animation on the capture label.
@@ -250,8 +264,8 @@
 
   // Ensures that the bar widget is on top of everything, and the overlay (which
   // is the |layer()| of this class that paints the capture region) is stacked
-  // right below the bar.
-  void RefreshStackingOrder(aura::Window* parent_container);
+  // below the bar.
+  void RefreshStackingOrder();
 
   // Paints the current capture region depending on the current capture source.
   void PaintCaptureRegion(gfx::Canvas* canvas);
@@ -443,6 +457,9 @@
   // Observer to observe the current selected to-be-captured window.
   std::unique_ptr<CaptureWindowObserver> capture_window_observer_;
 
+  // Observer to observe the parent container `kShellWindowId_MenuContainer`.
+  std::unique_ptr<ParentContainerObserver> parent_container_observer_;
+
   // Contains the window dimmers which dim all the root windows except
   // |current_root_|.
   base::flat_set<std::unique_ptr<WindowDimmer>> root_window_dimmers_;
diff --git a/ash/capture_mode/capture_window_observer.cc b/ash/capture_mode/capture_window_observer.cc
index 9a05784..c0c153b 100644
--- a/ash/capture_mode/capture_window_observer.cc
+++ b/ash/capture_mode/capture_window_observer.cc
@@ -75,6 +75,8 @@
   auto* camera_controller = controller->camera_controller();
   if (camera_controller && !controller->is_recording_in_progress())
     camera_controller->MaybeReparentPreviewWidget();
+  capture_mode_session_->MaybeUpdateCaptureUisOpacity(
+      display::Screen::GetScreen()->GetCursorScreenPoint());
 }
 
 void CaptureWindowObserver::OnWindowBoundsChanged(
diff --git a/ash/capture_mode/video_recording_watcher.cc b/ash/capture_mode/video_recording_watcher.cc
index 7809d593..aea351ef 100644
--- a/ash/capture_mode/video_recording_watcher.cc
+++ b/ash/capture_mode/video_recording_watcher.cc
@@ -283,7 +283,7 @@
   DCHECK(window_being_recorded_);
   return window_being_recorded_->IsRootWindow()
              ? window_being_recorded_->GetChildById(
-                   kShellWindowId_OverlayContainer)
+                   kShellWindowId_MenuContainer)
              : window_being_recorded_;
 }
 
diff --git a/ash/components/device_activity/device_activity_client.cc b/ash/components/device_activity/device_activity_client.cc
index 94aa06f..177380b 100644
--- a/ash/components/device_activity/device_activity_client.cc
+++ b/ash/components/device_activity/device_activity_client.cc
@@ -63,11 +63,11 @@
 
 // Record the minute the device activity client transitions out of idle.
 const char kDeviceActiveClientTransitionOutOfIdleMinute[] =
-    "Ash.DeviceActiveClient.TransitionOutOfIdleMinute";
+    "Ash.DeviceActiveClient.RecordedTransitionOutOfIdleMinute";
 
 // Record the minute the device activity client transitions to check in.
 const char kDeviceActiveClientTransitionToCheckInMinute[] =
-    "Ash.DeviceActiveClient.TransitionToCheckInMinute";
+    "Ash.DeviceActiveClient.RecordedTransitionToCheckInMinute";
 
 // Generates the full histogram name for histogram variants based on state.
 std::string HistogramVariantName(const std::string& histogram_prefix,
@@ -99,28 +99,26 @@
 }
 
 // Return the minute of the current UTC time.
-base::TimeDelta GetCurrentMinute() {
+int GetCurrentMinute() {
   base::Time cur_time = base::Time::Now();
 
   // Extract minute from exploded |cur_time| in UTC.
   base::Time::Exploded exploded_utc;
   cur_time.UTCExplode(&exploded_utc);
 
-  return base::Minutes(exploded_utc.minute);
+  return exploded_utc.minute;
 }
 
 void RecordTransitionOutOfIdleMinute() {
-  base::UmaHistogramCustomTimes(kDeviceActiveClientTransitionOutOfIdleMinute,
-                                GetCurrentMinute(), base::Minutes(0),
-                                base::Minutes(59),
-                                60 /* number of histogram buckets */);
+  base::UmaHistogramCustomCounts(kDeviceActiveClientTransitionOutOfIdleMinute,
+                                 GetCurrentMinute(), 0, 59,
+                                 60 /* number of histogram buckets */);
 }
 
 void RecordTransitionToCheckInMinute() {
-  base::UmaHistogramCustomTimes(kDeviceActiveClientTransitionToCheckInMinute,
-                                GetCurrentMinute(), base::Minutes(0),
-                                base::Minutes(59),
-                                60 /* number of histogram buckets */);
+  base::UmaHistogramCustomCounts(kDeviceActiveClientTransitionToCheckInMinute,
+                                 GetCurrentMinute(), 0, 59,
+                                 60 /* number of histogram buckets */);
 }
 
 // Histogram sliced by duration and state.
diff --git a/ash/components/phonehub/BUILD.gn b/ash/components/phonehub/BUILD.gn
index 601dc8cf..9826010 100644
--- a/ash/components/phonehub/BUILD.gn
+++ b/ash/components/phonehub/BUILD.gn
@@ -26,6 +26,8 @@
     "camera_roll_thumbnail_decoder.h",
     "camera_roll_thumbnail_decoder_impl.cc",
     "camera_roll_thumbnail_decoder_impl.h",
+    "combined_access_setup_operation.cc",
+    "combined_access_setup_operation.h",
     "connection_scheduler.h",
     "connection_scheduler_impl.cc",
     "connection_scheduler_impl.h",
diff --git a/ash/components/phonehub/combined_access_setup_operation.cc b/ash/components/phonehub/combined_access_setup_operation.cc
new file mode 100644
index 0000000..d781859
--- /dev/null
+++ b/ash/components/phonehub/combined_access_setup_operation.cc
@@ -0,0 +1,82 @@
+// 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 "ash/components/phonehub/combined_access_setup_operation.h"
+
+#include <array>
+
+#include "base/check.h"
+#include "base/containers/contains.h"
+
+namespace ash {
+namespace phonehub {
+namespace {
+
+// Status values which are considered "final" - i.e., once the status of an
+// operation changes to one of these values, the operation has completed. These
+// status values indicate either a success or a fatal error.
+constexpr std::array<CombinedAccessSetupOperation::Status, 4>
+    kOperationFinishedStatus{
+        CombinedAccessSetupOperation::Status::kTimedOutConnecting,
+        CombinedAccessSetupOperation::Status::kConnectionDisconnected,
+        CombinedAccessSetupOperation::Status::kCompletedSuccessfully,
+        CombinedAccessSetupOperation::Status::kProhibitedFromProvidingAccess,
+    };
+
+}  // namespace
+
+// static
+bool CombinedAccessSetupOperation::IsFinalStatus(Status status) {
+  return base::Contains(kOperationFinishedStatus, status);
+}
+
+CombinedAccessSetupOperation::CombinedAccessSetupOperation(
+    Delegate* delegate,
+    base::OnceClosure destructor_callback)
+    : delegate_(delegate),
+      destructor_callback_(std::move(destructor_callback)) {
+  DCHECK(delegate_);
+  DCHECK(destructor_callback_);
+}
+
+CombinedAccessSetupOperation::~CombinedAccessSetupOperation() {
+  std::move(destructor_callback_).Run();
+}
+
+void CombinedAccessSetupOperation::NotifyCombinedStatusChanged(
+    Status new_status) {
+  current_status_ = new_status;
+
+  delegate_->OnCombinedStatusChange(new_status);
+}
+
+std::ostream& operator<<(std::ostream& stream,
+                         CombinedAccessSetupOperation::Status status) {
+  switch (status) {
+    case CombinedAccessSetupOperation::Status::kConnecting:
+      stream << "[Connecting]";
+      break;
+    case CombinedAccessSetupOperation::Status::kTimedOutConnecting:
+      stream << "[Timed out connecting]";
+      break;
+    case CombinedAccessSetupOperation::Status::kConnectionDisconnected:
+      stream << "[Connection disconnected]";
+      break;
+    case CombinedAccessSetupOperation::Status::
+        kSentMessageToPhoneAndWaitingForResponse:
+      stream << "[Sent message to phone; waiting for response]";
+      break;
+    case CombinedAccessSetupOperation::Status::kCompletedSuccessfully:
+      stream << "[Completed successfully]";
+      break;
+    case CombinedAccessSetupOperation::Status::kProhibitedFromProvidingAccess:
+      stream << "[Prohibited from providing access]";
+      break;
+  }
+
+  return stream;
+}
+
+}  // namespace phonehub
+}  // namespace ash
diff --git a/ash/components/phonehub/combined_access_setup_operation.h b/ash/components/phonehub/combined_access_setup_operation.h
new file mode 100644
index 0000000..5171ddb5
--- /dev/null
+++ b/ash/components/phonehub/combined_access_setup_operation.h
@@ -0,0 +1,105 @@
+// 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 ASH_COMPONENTS_PHONEHUB_COMBINED_ACCESS_SETUP_OPERATION_H_
+#define ASH_COMPONENTS_PHONEHUB_COMBINED_ACCESS_SETUP_OPERATION_H_
+
+#include <ostream>
+
+#include "base/callback.h"
+#include "third_party/abseil-cpp/absl/types/optional.h"
+
+namespace ash {
+namespace phonehub {
+
+// Implements the combined access setup flow. This flow involves:
+// (1) Creating a connection to the phone if one does not already exist.
+// (2) Sending a message to the phone which asks it to begin the setup flow;
+//     upon receipt of the message, the phone displays a UI which asks the
+//     user to enable permissions for the Phone Hub features that need setup.
+// (3) Waiting for the user to complete the flow; once the flow is complete, the
+//     phone sends a message back to this device which indicates that permission
+//     has been granted.
+//
+// If an instance of this class exists, the flow continues until the status
+// changes to a "final" status (i.e., a success or a fatal error). To cancel the
+// ongoing setup operation, simply delete the instance of this class.
+class CombinedAccessSetupOperation {
+ public:
+  // Note: Numerical values should not be changed because they must stay in
+  // sync with multidevice_permissions_access_setup_dialog.js, with the
+  // exception of NOT_STARTED, which has a value of 0. Also, these values are
+  // persisted to logs. Entries should not be renumbered and numeric values
+  // should never be reused. If entries are added, kMaxValue should be updated.
+  enum class Status {
+    // Connecting to the phone in order to set up feature access.
+    kConnecting = 1,
+
+    // No connection was able to be made to the phone within the expected time
+    // period.
+    kTimedOutConnecting = 2,
+
+    // A connection to the phone was successful, but it unexpectedly became
+    // disconnected before the setup flow could complete.
+    kConnectionDisconnected = 3,
+
+    // A connection to the phone has succeeded, and a message has been sent to
+    // the phone to start the feature access opt-in flow. However, the user
+    // has not yet completed the flow phone-side.
+    kSentMessageToPhoneAndWaitingForResponse = 4,
+
+    // The user has completed the phone-side opt-in flow.
+    kCompletedSuccessfully = 5,
+
+    // The user's phone is prohibited from granting feature access (e.g.,
+    // the user could be using a Work Profile).
+    kProhibitedFromProvidingAccess = 6,
+
+    kMaxValue = kProhibitedFromProvidingAccess
+  };
+
+  // Returns true if the provided status is the final one for this operation,
+  // indicating either success or failure.
+  static bool IsFinalStatus(Status status);
+
+  class Delegate {
+   public:
+    virtual ~Delegate() = default;
+
+    // Called when status of the setup flow has changed.
+    virtual void OnCombinedStatusChange(Status new_status) = 0;
+  };
+
+  CombinedAccessSetupOperation(const CombinedAccessSetupOperation&) = delete;
+  CombinedAccessSetupOperation& operator=(const CombinedAccessSetupOperation&) =
+      delete;
+  virtual ~CombinedAccessSetupOperation();
+
+ private:
+  friend class MultideviceFeatureAccessManager;
+
+  CombinedAccessSetupOperation(Delegate* delegate,
+                               base::OnceClosure destructor_callback);
+
+  void NotifyCombinedStatusChanged(Status new_status);
+
+  absl::optional<Status> current_status_;
+  Delegate* const delegate_;
+  base::OnceClosure destructor_callback_;
+};
+
+std::ostream& operator<<(std::ostream& stream,
+                         CombinedAccessSetupOperation::Status status);
+
+}  // namespace phonehub
+}  // namespace ash
+
+// TODO(https://crbug.com/1164001): remove after the migration is finished.
+namespace chromeos {
+namespace phonehub {
+using ::ash::phonehub::CombinedAccessSetupOperation;
+}
+}  // namespace chromeos
+
+#endif  // ASH_COMPONENTS_PHONEHUB_COMBINED_ACCESS_SETUP_OPERATION_H_
diff --git a/ash/components/phonehub/fake_message_sender.cc b/ash/components/phonehub/fake_message_sender.cc
index b1db0560..b4d9879 100644
--- a/ash/components/phonehub/fake_message_sender.cc
+++ b/ash/components/phonehub/fake_message_sender.cc
@@ -61,6 +61,11 @@
   initiate_camera_roll_item_transfer_requests_.push_back(request);
 }
 
+void FakeMessageSender::SendFeatureSetupRequest(bool camera_roll,
+                                                bool notifications) {
+  feature_setup_requests_.push_back(std::make_pair(camera_roll, notifications));
+}
+
 size_t FakeMessageSender::GetCrosStateCallCount() const {
   return cros_states_.size();
 }
@@ -98,6 +103,10 @@
   return initiate_camera_roll_item_transfer_requests_.size();
 }
 
+size_t FakeMessageSender::GetFeatureSetupRequestCallCount() const {
+  return feature_setup_requests_.size();
+}
+
 std::pair<bool, bool> FakeMessageSender::GetRecentCrosState() const {
   return cros_states_.back();
 }
@@ -138,5 +147,9 @@
   return initiate_camera_roll_item_transfer_requests_.back();
 }
 
+std::pair<bool, bool> FakeMessageSender::GetRecentFeatureSetupRequest() const {
+  return feature_setup_requests_.back();
+}
+
 }  // namespace phonehub
 }  // namespace ash
diff --git a/ash/components/phonehub/fake_message_sender.h b/ash/components/phonehub/fake_message_sender.h
index 05c5ded..3756f1c 100644
--- a/ash/components/phonehub/fake_message_sender.h
+++ b/ash/components/phonehub/fake_message_sender.h
@@ -38,6 +38,7 @@
       const proto::FetchCameraRollItemDataRequest& request) override;
   void SendInitiateCameraRollItemTransferRequest(
       const proto::InitiateCameraRollItemTransferRequest& request) override;
+  void SendFeatureSetupRequest(bool camera_roll, bool notifications) override;
 
   std::pair<bool, bool> GetRecentCrosState() const;
   bool GetRecentUpdateNotificationModeRequest() const;
@@ -52,6 +53,7 @@
   GetRecentFetchCameraRollItemDataRequest() const;
   const proto::InitiateCameraRollItemTransferRequest&
   GetRecentInitiateCameraRollItemTransferRequest() const;
+  std::pair<bool, bool> GetRecentFeatureSetupRequest() const;
 
   size_t GetCrosStateCallCount() const;
 
@@ -75,6 +77,8 @@
 
   size_t GetInitiateCameraRollItemTransferRequestCallCount() const;
 
+  size_t GetFeatureSetupRequestCallCount() const;
+
  private:
   std::vector<std::pair</*is_notifications_setting_enabled*/ bool,
                         /*is_camera_roll_setting_enabled*/ bool>>
@@ -92,6 +96,7 @@
   std::vector<proto::InitiateCameraRollItemTransferRequest>
       initiate_camera_roll_item_transfer_requests_;
   size_t show_notification_access_setup_count_ = 0;
+  std::vector<std::pair<bool, bool>> feature_setup_requests_;
 };
 
 }  // namespace phonehub
diff --git a/ash/components/phonehub/fake_multidevice_feature_access_manager.cc b/ash/components/phonehub/fake_multidevice_feature_access_manager.cc
index e0627d5..44bd89c 100644
--- a/ash/components/phonehub/fake_multidevice_feature_access_manager.cc
+++ b/ash/components/phonehub/fake_multidevice_feature_access_manager.cc
@@ -119,5 +119,24 @@
       new_status);
 }
 
+void FakeMultideviceFeatureAccessManager::SetCombinedSetupOperationStatus(
+    CombinedAccessSetupOperation::Status new_status) {
+  if (new_status ==
+      CombinedAccessSetupOperation::Status::kCompletedSuccessfully) {
+    SetCameraRollAccessStatusInternal(AccessStatus::kAccessGranted);
+  }
+  MultideviceFeatureAccessManager::SetCombinedSetupOperationStatus(new_status);
+}
+
+void FakeMultideviceFeatureAccessManager::
+    SetFeatureSetupRequestSupportedInternal(bool supported) {
+  is_feature_setup_request_supported_ = supported;
+}
+
+bool FakeMultideviceFeatureAccessManager::GetFeatureSetupRequestSupported()
+    const {
+  return is_feature_setup_request_supported_;
+}
+
 }  // namespace phonehub
 }  // namespace ash
diff --git a/ash/components/phonehub/fake_multidevice_feature_access_manager.h b/ash/components/phonehub/fake_multidevice_feature_access_manager.h
index 1262d70..daa823db 100644
--- a/ash/components/phonehub/fake_multidevice_feature_access_manager.h
+++ b/ash/components/phonehub/fake_multidevice_feature_access_manager.h
@@ -31,7 +31,8 @@
       AccessProhibitedReason reason = AccessProhibitedReason::kWorkProfile);
   ~FakeMultideviceFeatureAccessManager() override;
 
-  using MultideviceFeatureAccessManager::IsSetupOperationInProgress;
+  using MultideviceFeatureAccessManager::IsCombinedSetupOperationInProgress;
+  using MultideviceFeatureAccessManager::IsNotificationSetupOperationInProgress;
 
   void SetNotificationAccessStatusInternal(
       AccessStatus notification_access_status,
@@ -48,6 +49,9 @@
   void SetCameraRollAccessStatusInternal(
       AccessStatus camera_roll_access_status) override;
   AccessStatus GetCameraRollAccessStatus() const override;
+  void SetCombinedSetupOperationStatus(
+      CombinedAccessSetupOperation::Status new_status);
+
   AccessStatus GetAppsAccessStatus() const override;
   bool IsAccessRequestAllowed(Feature feature) override;
 
@@ -55,6 +59,9 @@
   void SetAppsAccessStatusInternal(AccessStatus apps_access_status);
   void SetFeatureReadyForAccess(Feature feature);
 
+  void SetFeatureSetupRequestSupportedInternal(bool supported) override;
+  bool GetFeatureSetupRequestSupported() const override;
+
  private:
   AccessStatus notification_access_status_;
   AccessStatus camera_roll_access_status_;
@@ -62,6 +69,7 @@
   AccessProhibitedReason access_prohibited_reason_;
   bool has_notification_setup_ui_been_dismissed_ = false;
   std::vector<Feature> ready_for_access_features_;
+  bool is_feature_setup_request_supported_ = false;
 };
 
 }  // namespace phonehub
diff --git a/ash/components/phonehub/message_sender.h b/ash/components/phonehub/message_sender.h
index f7b4053..f0fcf91 100644
--- a/ash/components/phonehub/message_sender.h
+++ b/ash/components/phonehub/message_sender.h
@@ -46,6 +46,10 @@
   // Requests that the phone should show the notification access set up.
   virtual void SendShowNotificationAccessSetupRequest() = 0;
 
+  // Requests that the phone should show the feature access set up.
+  virtual void SendFeatureSetupRequest(bool camera_roll,
+                                       bool notifications) = 0;
+
   // Requests that the phone enables or disables ringing.
   virtual void SendRingDeviceRequest(bool device_ringing_enabled) = 0;
 
diff --git a/ash/components/phonehub/message_sender_impl.cc b/ash/components/phonehub/message_sender_impl.cc
index 4e97975..ca28503 100644
--- a/ash/components/phonehub/message_sender_impl.cc
+++ b/ash/components/phonehub/message_sender_impl.cc
@@ -110,6 +110,15 @@
               &request);
 }
 
+void MessageSenderImpl::SendFeatureSetupRequest(bool camera_roll,
+                                                bool notifications) {
+  proto::FeatureSetupRequest request;
+  request.set_camera_roll_setup_requested(camera_roll);
+  request.set_notification_setup_requested(notifications);
+
+  SendMessage(proto::MessageType::FEATURE_SETUP_REQUEST, &request);
+}
+
 void MessageSenderImpl::SendRingDeviceRequest(bool device_ringing_enabled) {
   proto::FindMyDeviceRingStatus ringing_enabled =
       device_ringing_enabled ? proto::FindMyDeviceRingStatus::RINGING
diff --git a/ash/components/phonehub/message_sender_impl.h b/ash/components/phonehub/message_sender_impl.h
index 248d985..a9998ff 100644
--- a/ash/components/phonehub/message_sender_impl.h
+++ b/ash/components/phonehub/message_sender_impl.h
@@ -33,6 +33,7 @@
       int64_t notification_id,
       const std::u16string& reply_text) override;
   void SendShowNotificationAccessSetupRequest() override;
+  void SendFeatureSetupRequest(bool camera_roll, bool notifications) override;
   void SendRingDeviceRequest(bool device_ringing_enabled) override;
   void SendFetchCameraRollItemsRequest(
       const proto::FetchCameraRollItemsRequest& request) override;
diff --git a/ash/components/phonehub/message_sender_unittest.cc b/ash/components/phonehub/message_sender_unittest.cc
index 04a5dd6..41893e9 100644
--- a/ash/components/phonehub/message_sender_unittest.cc
+++ b/ash/components/phonehub/message_sender_unittest.cc
@@ -173,5 +173,17 @@
                 &request, fake_connection_manager_->sent_messages().back());
 }
 
+TEST_F(MessageSenderImplTest, SendFeatureSetupRequest) {
+  proto::FeatureSetupRequest request;
+  request.set_camera_roll_setup_requested(true);
+  request.set_notification_setup_requested(true);
+
+  message_sender_->SendFeatureSetupRequest(/*camera_roll=*/true,
+                                           /*notifications=*/true);
+
+  VerifyMessage(proto::MessageType::FEATURE_SETUP_REQUEST, &request,
+                fake_connection_manager_->sent_messages().back());
+}
+
 }  // namespace phonehub
 }  // namespace ash
diff --git a/ash/components/phonehub/multidevice_feature_access_manager.cc b/ash/components/phonehub/multidevice_feature_access_manager.cc
index 5ef4348..2c068560 100644
--- a/ash/components/phonehub/multidevice_feature_access_manager.cc
+++ b/ash/components/phonehub/multidevice_feature_access_manager.cc
@@ -20,7 +20,7 @@
     NotificationAccessSetupOperation::Delegate* delegate) {
   // Should only be able to start the setup process if notification access is
   // available but not yet granted.
-  // TODO: check camra roll access status once setup flow is wired up
+  // Legacy setup flow used when FeatureSetupRequest is not supported.
   if (GetNotificationAccessStatus() != AccessStatus::kAvailableButNotGranted)
     return nullptr;
 
@@ -29,11 +29,45 @@
 
   auto operation = base::WrapUnique(new NotificationAccessSetupOperation(
       delegate,
-      base::BindOnce(&MultideviceFeatureAccessManager::OnSetupOperationDeleted,
-                     weak_ptr_factory_.GetWeakPtr(), operation_id)));
-  id_to_operation_map_.emplace(operation_id, operation.get());
+      base::BindOnce(
+          &MultideviceFeatureAccessManager::OnNotificationSetupOperationDeleted,
+          weak_ptr_factory_.GetWeakPtr(), operation_id)));
+  id_to_notification_operation_map_.emplace(operation_id, operation.get());
 
-  OnSetupRequested();
+  OnNotificationSetupRequested();
+  return operation;
+}
+
+std::unique_ptr<CombinedAccessSetupOperation>
+MultideviceFeatureAccessManager::AttemptCombinedFeatureSetup(
+    bool camera_roll,
+    bool notifications,
+    CombinedAccessSetupOperation::Delegate* delegate) {
+  // New setup flow for combined Camera Roll and/or Notifications setup using
+  // FeatureSetupRequest message type.
+  if (!GetFeatureSetupRequestSupported()) {
+    return nullptr;
+  }
+  if (GetCameraRollAccessStatus() != AccessStatus::kAvailableButNotGranted &&
+      camera_roll) {
+    return nullptr;
+  }
+  if (GetNotificationAccessStatus() != AccessStatus::kAvailableButNotGranted &&
+      notifications) {
+    return nullptr;
+  }
+
+  int operation_id = next_operation_id_;
+  ++next_operation_id_;
+
+  auto operation = base::WrapUnique(new CombinedAccessSetupOperation(
+      delegate,
+      base::BindOnce(
+          &MultideviceFeatureAccessManager::OnCombinedSetupOperationDeleted,
+          weak_ptr_factory_.GetWeakPtr(), operation_id)));
+  id_to_combined_operation_map_.emplace(operation_id, operation.get());
+
+  OnCombinedSetupRequested(camera_roll, notifications);
   return operation;
 }
 
@@ -60,35 +94,78 @@
     observer.OnAppsAccessChanged();
 }
 
+void MultideviceFeatureAccessManager::
+    NotifyFeatureSetupRequestSupportedChanged() {
+  for (auto& observer : observer_list_)
+    observer.OnFeatureSetupRequestSupportedChanged();
+}
+
 void MultideviceFeatureAccessManager::SetNotificationSetupOperationStatus(
     NotificationAccessSetupOperation::Status new_status) {
-  DCHECK(IsSetupOperationInProgress());
+  DCHECK(IsNotificationSetupOperationInProgress());
 
   PA_LOG(INFO) << "Notification access setup flow - new status: " << new_status;
 
-  for (auto& it : id_to_operation_map_)
-    it.second->NotifyStatusChanged(new_status);
+  for (auto& it : id_to_notification_operation_map_)
+    it.second->NotifyNotificationStatusChanged(new_status);
 
   if (NotificationAccessSetupOperation::IsFinalStatus(new_status))
-    id_to_operation_map_.clear();
+    id_to_notification_operation_map_.clear();
 }
 
-bool MultideviceFeatureAccessManager::IsSetupOperationInProgress() const {
-  return !id_to_operation_map_.empty();
+bool MultideviceFeatureAccessManager::IsNotificationSetupOperationInProgress()
+    const {
+  return !id_to_notification_operation_map_.empty();
 }
 
-void MultideviceFeatureAccessManager::OnSetupOperationDeleted(
+void MultideviceFeatureAccessManager::OnNotificationSetupOperationDeleted(
     int operation_id) {
-  auto it = id_to_operation_map_.find(operation_id);
-  if (it == id_to_operation_map_.end())
+  auto it = id_to_notification_operation_map_.find(operation_id);
+  if (it == id_to_notification_operation_map_.end())
     return;
 
-  id_to_operation_map_.erase(it);
+  id_to_notification_operation_map_.erase(it);
 
-  if (id_to_operation_map_.empty())
+  if (id_to_notification_operation_map_.empty())
     PA_LOG(INFO) << "Notification access setup operation has ended.";
 }
 
+void MultideviceFeatureAccessManager::SetCombinedSetupOperationStatus(
+    CombinedAccessSetupOperation::Status new_status) {
+  DCHECK(IsCombinedSetupOperationInProgress());
+
+  PA_LOG(INFO) << "Combined access setup flow - new status: " << new_status;
+
+  for (auto& it : id_to_combined_operation_map_)
+    it.second->NotifyCombinedStatusChanged(new_status);
+
+  if (CombinedAccessSetupOperation::IsFinalStatus(new_status))
+    id_to_combined_operation_map_.clear();
+}
+
+bool MultideviceFeatureAccessManager::IsCombinedSetupOperationInProgress()
+    const {
+  return !id_to_combined_operation_map_.empty();
+}
+
+void MultideviceFeatureAccessManager::OnNotificationSetupRequested() {}
+
+void MultideviceFeatureAccessManager::OnCombinedSetupRequested(
+    bool camera_roll,
+    bool notifications) {}
+
+void MultideviceFeatureAccessManager::OnCombinedSetupOperationDeleted(
+    int operation_id) {
+  auto it = id_to_combined_operation_map_.find(operation_id);
+  if (it == id_to_combined_operation_map_.end())
+    return;
+
+  id_to_combined_operation_map_.erase(it);
+
+  if (id_to_combined_operation_map_.empty())
+    PA_LOG(INFO) << "Combined access setup operation has ended.";
+}
+
 void MultideviceFeatureAccessManager::Observer::OnNotificationAccessChanged() {
   // Optional method, inherit class doesn't have to implement this
 }
@@ -101,6 +178,11 @@
   // Optional method, inherit class doesn't have to implement this
 }
 
+void MultideviceFeatureAccessManager::Observer::
+    OnFeatureSetupRequestSupportedChanged() {
+  // Optional method, inherit class doesn't have to implement this
+}
+
 std::ostream& operator<<(std::ostream& stream,
                          MultideviceFeatureAccessManager::AccessStatus status) {
   switch (status) {
diff --git a/ash/components/phonehub/multidevice_feature_access_manager.h b/ash/components/phonehub/multidevice_feature_access_manager.h
index e61e32e..7d46f8c 100644
--- a/ash/components/phonehub/multidevice_feature_access_manager.h
+++ b/ash/components/phonehub/multidevice_feature_access_manager.h
@@ -7,6 +7,7 @@
 
 #include <ostream>
 
+#include "ash/components/phonehub/combined_access_setup_operation.h"
 #include "ash/components/phonehub/notification_access_setup_operation.h"
 #include "ash/services/multidevice_setup/public/mojom/multidevice_setup.mojom.h"
 #include "base/containers/flat_map.h"
@@ -28,12 +29,15 @@
 // has not yet been granted. If there is no active Phone Hub connection, we
 // assume that the last access value seen is the current value.
 //
-// Additionally, this class provides an API for requesting the notification
-// access setup flow via AttemptNotificationSetup().
+// This class provides two methods for requesting access permissions on the
+// connected Android device:
 //
-// In order for user to use camera roll feature, users need to explicit give
-// consent for the feature to be enabled on the phone via the feature setup
-// dialog.
+// AttemptNotificationSetup() is the legacy setup flow that only supports setup
+// of the Notifications feature.
+//
+// AttemptCombinedFeatureSetup() is the new setup flow that supports the
+// Notifications and/or Camera Roll features. New features requiring setup
+// should be added to this method's flow.
 class MultideviceFeatureAccessManager {
  public:
   // Status of a feature's access. Numerical values are stored in prefs and
@@ -77,6 +81,10 @@
     // Called when apps access has changed; use
     // GetAppsAccessStatus() for the new status.
     virtual void OnAppsAccessChanged();
+
+    // Called when FeatureSetupRequestSupported has changed; use
+    // GetFeatureSetupRequestSupported() for the new status.
+    virtual void OnFeatureSetupRequestSupportedChanged();
   };
 
   MultideviceFeatureAccessManager(MultideviceFeatureAccessManager&) = delete;
@@ -96,6 +104,8 @@
   virtual bool IsAccessRequestAllowed(
       multidevice_setup::mojom::Feature feature) = 0;
 
+  virtual bool GetFeatureSetupRequestSupported() const = 0;
+
   // Returns the reason notification access status is prohibited. The return
   // result is valid if the current access status (from GetAccessStatus())
   // is AccessStatus::kProhibited. Otherwise, the result is undefined and should
@@ -121,6 +131,22 @@
   std::unique_ptr<NotificationAccessSetupOperation> AttemptNotificationSetup(
       NotificationAccessSetupOperation::Delegate* delegate);
 
+  // Starts an attempt to enable the access for multiple features. |delegate|
+  // will be updated with the status of the flow as long as the operation object
+  // returned by this function remains instantiated.
+  //
+  // To cancel an ongoing setup attempt, delete the operation. If a setup
+  // attempt fails, clients can retry by calling AttemptCombinedFeatureSetup()
+  // again to start a new attempt.
+  //
+  // If a requested feature's access has already been granted, or the
+  // FeatureSetupRequest message is not supported on the phone, this function
+  // returns null.
+  std::unique_ptr<CombinedAccessSetupOperation> AttemptCombinedFeatureSetup(
+      bool camera_roll,
+      bool notifications,
+      CombinedAccessSetupOperation::Delegate* delegate);
+
   void AddObserver(Observer* observer);
   void RemoveObserver(Observer* observer);
 
@@ -130,12 +156,17 @@
   void NotifyNotificationAccessChanged();
   void NotifyCameraRollAccessChanged();
   void NotifyAppsAccessChanged();
+  void NotifyFeatureSetupRequestSupportedChanged();
   void SetNotificationSetupOperationStatus(
       NotificationAccessSetupOperation::Status new_status);
+  void SetCombinedSetupOperationStatus(
+      CombinedAccessSetupOperation::Status new_status);
 
-  bool IsSetupOperationInProgress() const;
+  bool IsNotificationSetupOperationInProgress() const;
+  bool IsCombinedSetupOperationInProgress() const;
 
-  virtual void OnSetupRequested() {}
+  virtual void OnNotificationSetupRequested();
+  virtual void OnCombinedSetupRequested(bool camera_roll, bool notifications);
 
  private:
   friend class MultideviceFeatureAccessManagerImplTest;
@@ -149,12 +180,19 @@
   // Sets the internal AccessStatus but does not send a request for
   // a new status to the remote phone device.
   virtual void SetCameraRollAccessStatusInternal(
-      AccessStatus camera_roll_access_status) = 0;
+      AccessStatus access_status) = 0;
+  // Sets internal status tracking if feature setup request message is
+  // supported by connected phone.
+  virtual void SetFeatureSetupRequestSupportedInternal(bool supported) = 0;
 
-  void OnSetupOperationDeleted(int operation_id);
+  void OnNotificationSetupOperationDeleted(int operation_id);
+  void OnCombinedSetupOperationDeleted(int operation_id);
 
   int next_operation_id_ = 0;
-  base::flat_map<int, NotificationAccessSetupOperation*> id_to_operation_map_;
+  base::flat_map<int, NotificationAccessSetupOperation*>
+      id_to_notification_operation_map_;
+  base::flat_map<int, CombinedAccessSetupOperation*>
+      id_to_combined_operation_map_;
   base::ObserverList<Observer> observer_list_;
   base::WeakPtrFactory<MultideviceFeatureAccessManager> weak_ptr_factory_{this};
 };
diff --git a/ash/components/phonehub/multidevice_feature_access_manager_impl.cc b/ash/components/phonehub/multidevice_feature_access_manager_impl.cc
index 26ed03d..bbd88897 100644
--- a/ash/components/phonehub/multidevice_feature_access_manager_impl.cc
+++ b/ash/components/phonehub/multidevice_feature_access_manager_impl.cc
@@ -42,6 +42,7 @@
   registry->RegisterBooleanPref(prefs::kHasDismissedSetupRequiredUi, false);
   registry->RegisterBooleanPref(prefs::kNeedsOneTimeNotificationAccessUpdate,
                                 true);
+  registry->RegisterBooleanPref(prefs::kFeatureSetupRequestSupported, false);
 }
 
 MultideviceFeatureAccessManagerImpl::MultideviceFeatureAccessManagerImpl(
@@ -114,6 +115,11 @@
   return static_cast<AccessStatus>(status);
 }
 
+bool MultideviceFeatureAccessManagerImpl::GetFeatureSetupRequestSupported()
+    const {
+  return pref_service_->GetBoolean(prefs::kFeatureSetupRequestSupported);
+}
+
 MultideviceFeatureAccessManagerImpl::AccessProhibitedReason
 MultideviceFeatureAccessManagerImpl::GetNotificationAccessProhibitedReason()
     const {
@@ -157,33 +163,80 @@
                             static_cast<int>(reason));
   NotifyNotificationAccessChanged();
 
-  if (!IsSetupOperationInProgress())
+  if (IsNotificationSetupOperationInProgress()) {
+    switch (access_status) {
+      case AccessStatus::kProhibited:
+        SetNotificationSetupOperationStatus(
+            NotificationAccessSetupOperation::Status::
+                kProhibitedFromProvidingAccess);
+        break;
+      case AccessStatus::kAccessGranted:
+        SetNotificationSetupOperationStatus(
+            NotificationAccessSetupOperation::Status::kCompletedSuccessfully);
+        break;
+      case AccessStatus::kAvailableButNotGranted:
+        // Intentionally blank; the operation status should not change.
+        break;
+    }
+  } else if (IsCombinedSetupOperationInProgress()) {
+    switch (access_status) {
+      case AccessStatus::kProhibited:
+        SetCombinedSetupOperationStatus(CombinedAccessSetupOperation::Status::
+                                            kProhibitedFromProvidingAccess);
+        break;
+      case AccessStatus::kAccessGranted:
+        combined_setup_notifications_pending_ = false;
+        break;
+      case AccessStatus::kAvailableButNotGranted:
+        // Intentionally blank; the operation status should not change.
+        break;
+    }
+    if (!combined_setup_notifications_pending_ &&
+        !combined_setup_camera_roll_pending_) {
+      SetCombinedSetupOperationStatus(
+          CombinedAccessSetupOperation::Status::kCompletedSuccessfully);
+    }
+  }
+}
+
+void MultideviceFeatureAccessManagerImpl::SetCameraRollAccessStatusInternal(
+    AccessStatus access_status) {
+  PA_LOG(INFO) << "Camera Roll access: " << GetCameraRollAccessStatus()
+               << " => " << access_status;
+  pref_service_->SetInteger(prefs::kCameraRollAccessStatus,
+                            static_cast<int>(access_status));
+  NotifyCameraRollAccessChanged();
+
+  if (!IsCombinedSetupOperationInProgress()) {
     return;
+  }
 
   switch (access_status) {
     case AccessStatus::kProhibited:
-      SetNotificationSetupOperationStatus(
-          NotificationAccessSetupOperation::Status::
-              kProhibitedFromProvidingAccess);
+      SetCombinedSetupOperationStatus(
+          CombinedAccessSetupOperation::Status::kProhibitedFromProvidingAccess);
       break;
     case AccessStatus::kAccessGranted:
-      SetNotificationSetupOperationStatus(
-          NotificationAccessSetupOperation::Status::kCompletedSuccessfully);
+      combined_setup_camera_roll_pending_ = false;
       break;
     case AccessStatus::kAvailableButNotGranted:
       // Intentionally blank; the operation status should not change.
       break;
   }
+  if (!combined_setup_notifications_pending_ &&
+      !combined_setup_camera_roll_pending_) {
+    SetCombinedSetupOperationStatus(
+        CombinedAccessSetupOperation::Status::kCompletedSuccessfully);
+  }
 }
 
-void MultideviceFeatureAccessManagerImpl::SetCameraRollAccessStatusInternal(
-    AccessStatus camera_roll_access_status) {
-  pref_service_->SetInteger(prefs::kCameraRollAccessStatus,
-                            static_cast<int>(camera_roll_access_status));
-  NotifyCameraRollAccessChanged();
+void MultideviceFeatureAccessManagerImpl::
+    SetFeatureSetupRequestSupportedInternal(bool supported) {
+  pref_service_->SetBoolean(prefs::kFeatureSetupRequestSupported, supported);
+  NotifyFeatureSetupRequestSupportedChanged();
 }
 
-void MultideviceFeatureAccessManagerImpl::OnSetupRequested() {
+void MultideviceFeatureAccessManagerImpl::OnNotificationSetupRequested() {
   PA_LOG(INFO) << "Notification access setup flow started.";
 
   switch (feature_status_provider_->GetStatus()) {
@@ -210,10 +263,47 @@
   }
 }
 
-void MultideviceFeatureAccessManagerImpl::OnFeatureStatusChanged() {
-  if (!IsSetupOperationInProgress())
-    return;
+void MultideviceFeatureAccessManagerImpl::OnCombinedSetupRequested(
+    bool camera_roll,
+    bool notifications) {
+  combined_setup_camera_roll_pending_ = camera_roll;
+  combined_setup_notifications_pending_ = notifications;
+  PA_LOG(INFO) << "Combined access setup flow started.";
 
+  switch (feature_status_provider_->GetStatus()) {
+    // We're already connected, so request that the UI be shown on the phone.
+    case FeatureStatus::kEnabledAndConnected:
+      SendShowCombinedAccessSetupRequest();
+      break;
+    // We're already connecting, so wait until a connection succeeds before
+    // trying to send a message
+    case FeatureStatus::kEnabledAndConnecting:
+      SetCombinedSetupOperationStatus(
+          CombinedAccessSetupOperation::Status::kConnecting);
+      break;
+    // We are not connected, so schedule a connection; once the
+    // connection succeeds, we'll send the message in OnFeatureStatusChanged().
+    case FeatureStatus::kEnabledButDisconnected:
+      SetCombinedSetupOperationStatus(
+          CombinedAccessSetupOperation::Status::kConnecting);
+      connection_scheduler_->ScheduleConnectionNow();
+      break;
+    default:
+      NOTREACHED();
+      break;
+  }
+}
+
+void MultideviceFeatureAccessManagerImpl::OnFeatureStatusChanged() {
+  if (IsNotificationSetupOperationInProgress()) {
+    FeatureStatusChangedNotificationAccessSetup();
+  } else if (IsCombinedSetupOperationInProgress()) {
+    FeatureStatusChangedCombinedAccessSetup();
+  }
+}
+
+void MultideviceFeatureAccessManagerImpl::
+    FeatureStatusChangedNotificationAccessSetup() {
   const FeatureStatus previous_feature_status = current_feature_status_;
   current_feature_status_ = feature_status_provider_->GetStatus();
 
@@ -245,6 +335,38 @@
 }
 
 void MultideviceFeatureAccessManagerImpl::
+    FeatureStatusChangedCombinedAccessSetup() {
+  const FeatureStatus previous_feature_status = current_feature_status_;
+  current_feature_status_ = feature_status_provider_->GetStatus();
+
+  if (previous_feature_status == current_feature_status_)
+    return;
+
+  // If we were previously connecting and could not establish a connection,
+  // send a timeout state.
+  if (previous_feature_status == FeatureStatus::kEnabledAndConnecting &&
+      current_feature_status_ != FeatureStatus::kEnabledAndConnected) {
+    SetCombinedSetupOperationStatus(
+        CombinedAccessSetupOperation::Status::kTimedOutConnecting);
+    return;
+  }
+
+  // If we were previously connected and are now no longer connected, send a
+  // connection disconnected state.
+  if (previous_feature_status == FeatureStatus::kEnabledAndConnected &&
+      current_feature_status_ != FeatureStatus::kEnabledAndConnected) {
+    SetCombinedSetupOperationStatus(
+        CombinedAccessSetupOperation::Status::kConnectionDisconnected);
+    return;
+  }
+
+  if (current_feature_status_ == FeatureStatus::kEnabledAndConnected) {
+    SendShowCombinedAccessSetupRequest();
+    return;
+  }
+}
+
+void MultideviceFeatureAccessManagerImpl::
     SendShowNotificationAccessSetupRequest() {
   message_sender_->SendShowNotificationAccessSetupRequest();
   SetNotificationSetupOperationStatus(
@@ -252,6 +374,14 @@
           kSentMessageToPhoneAndWaitingForResponse);
 }
 
+void MultideviceFeatureAccessManagerImpl::SendShowCombinedAccessSetupRequest() {
+  message_sender_->SendFeatureSetupRequest(
+      combined_setup_camera_roll_pending_,
+      combined_setup_notifications_pending_);
+  SetCombinedSetupOperationStatus(CombinedAccessSetupOperation::Status::
+                                      kSentMessageToPhoneAndWaitingForResponse);
+}
+
 bool MultideviceFeatureAccessManagerImpl::HasAccessStatusChanged(
     AccessStatus access_status,
     AccessProhibitedReason reason) {
diff --git a/ash/components/phonehub/multidevice_feature_access_manager_impl.h b/ash/components/phonehub/multidevice_feature_access_manager_impl.h
index a3098b5b..9348649fc 100644
--- a/ash/components/phonehub/multidevice_feature_access_manager_impl.h
+++ b/ash/components/phonehub/multidevice_feature_access_manager_impl.h
@@ -45,15 +45,17 @@
   AccessStatus GetNotificationAccessStatus() const override;
   AccessProhibitedReason GetNotificationAccessProhibitedReason() const override;
   void SetNotificationAccessStatusInternal(
-      AccessStatus notification_access_status,
+      AccessStatus access_status,
       AccessProhibitedReason reason) override;
-  void SetCameraRollAccessStatusInternal(
-      AccessStatus camera_roll_access_status) override;
+  void SetCameraRollAccessStatusInternal(AccessStatus access_status) override;
   AccessStatus GetCameraRollAccessStatus() const override;
   AccessStatus GetAppsAccessStatus() const override;
   bool IsAccessRequestAllowed(
       multidevice_setup::mojom::Feature feature) override;
-  void OnSetupRequested() override;
+  bool GetFeatureSetupRequestSupported() const override;
+  void SetFeatureSetupRequestSupportedInternal(bool supported) override;
+  void OnNotificationSetupRequested() override;
+  void OnCombinedSetupRequested(bool camera_roll, bool notifications) override;
 
   bool HasMultideviceFeatureSetupUiBeenDismissed() const override;
   void DismissSetupRequiredUi() override;
@@ -61,7 +63,11 @@
   // FeatureStatusProvider::Observer:
   void OnFeatureStatusChanged() override;
 
+  void FeatureStatusChangedNotificationAccessSetup();
+  void FeatureStatusChangedCombinedAccessSetup();
+
   void SendShowNotificationAccessSetupRequest();
+  void SendShowCombinedAccessSetupRequest();
 
   bool HasAccessStatusChanged(AccessStatus access_status,
                               AccessProhibitedReason reason);
@@ -75,6 +81,9 @@
 
   // Registers preference value change listeners.
   PrefChangeRegistrar pref_change_registrar_;
+
+  bool combined_setup_notifications_pending_ = false;
+  bool combined_setup_camera_roll_pending_ = false;
 };
 
 }  // namespace phonehub
diff --git a/ash/components/phonehub/multidevice_feature_access_manager_impl_unittest.cc b/ash/components/phonehub/multidevice_feature_access_manager_impl_unittest.cc
index 945c7d7..e8c1a69 100644
--- a/ash/components/phonehub/multidevice_feature_access_manager_impl_unittest.cc
+++ b/ash/components/phonehub/multidevice_feature_access_manager_impl_unittest.cc
@@ -6,6 +6,7 @@
 
 #include <memory>
 
+#include "ash/components/phonehub/combined_access_setup_operation.h"
 #include "ash/components/phonehub/fake_connection_scheduler.h"
 #include "ash/components/phonehub/fake_feature_status_provider.h"
 #include "ash/components/phonehub/fake_message_sender.h"
@@ -41,22 +42,25 @@
   void OnCameraRollAccessChanged() override { ++num_calls_; }
 
   // MultideviceFeatureAccessManager::Observer:
+  void OnFeatureSetupRequestSupportedChanged() override { ++num_calls_; }
+
+  // MultideviceFeatureAccessManager::Observer:
   void OnAppsAccessChanged() override { ++num_calls_; }
 
  private:
   size_t num_calls_ = 0;
 };
 
-class FakeOperationDelegate
+class FakeNotificationAccessSetupOperationDelegate
     : public NotificationAccessSetupOperation::Delegate {
  public:
-  FakeOperationDelegate() = default;
-  ~FakeOperationDelegate() override = default;
+  FakeNotificationAccessSetupOperationDelegate() = default;
+  ~FakeNotificationAccessSetupOperationDelegate() override = default;
 
   NotificationAccessSetupOperation::Status status() const { return status_; }
 
   // NotificationAccessSetupOperation::Delegate:
-  void OnStatusChange(
+  void OnNotificationStatusChange(
       NotificationAccessSetupOperation::Status new_status) override {
     status_ = new_status;
   }
@@ -66,6 +70,25 @@
       NotificationAccessSetupOperation::Status::kConnecting;
 };
 
+class FakeCombinedAccessSetupOperationDelegate
+    : public CombinedAccessSetupOperation::Delegate {
+ public:
+  FakeCombinedAccessSetupOperationDelegate() = default;
+  ~FakeCombinedAccessSetupOperationDelegate() override = default;
+
+  CombinedAccessSetupOperation::Status status() const { return status_; }
+
+  // CombinedAccessSetupOperation::Delegate:
+  void OnCombinedStatusChange(
+      CombinedAccessSetupOperation::Status new_status) override {
+    status_ = new_status;
+  }
+
+ private:
+  CombinedAccessSetupOperation::Status status_ =
+      CombinedAccessSetupOperation::Status::kConnecting;
+};
+
 }  // namespace
 
 class MultideviceFeatureAccessManagerImplTest : public testing::Test {
@@ -97,13 +120,16 @@
   void InitializeAccessStatus(
       AccessStatus notification_expected_status,
       AccessStatus camera_roll_expected_status,
-      AccessProhibitedReason reason = AccessProhibitedReason::kUnknown) {
+      AccessProhibitedReason reason = AccessProhibitedReason::kUnknown,
+      bool feature_setup_request_supported = false) {
     pref_service_.SetInteger(prefs::kNotificationAccessStatus,
                              static_cast<int>(notification_expected_status));
     pref_service_.SetInteger(prefs::kCameraRollAccessStatus,
                              static_cast<int>(camera_roll_expected_status));
     pref_service_.SetInteger(prefs::kNotificationAccessProhibitedReason,
                              static_cast<int>(reason));
+    pref_service_.SetBoolean(prefs::kFeatureSetupRequestSupported,
+                             feature_setup_request_supported);
     SetNeedsOneTimeNotificationAccessUpdate(/*needs_update=*/false);
     manager_ = std::make_unique<MultideviceFeatureAccessManagerImpl>(
         &pref_service_, fake_multidevice_setup_client_.get(),
@@ -120,7 +146,11 @@
 
   NotificationAccessSetupOperation::Status
   GetNotificationAccessSetupOperationStatus() {
-    return fake_delegate_.status();
+    return fake_notification_delegate_.status();
+  }
+
+  CombinedAccessSetupOperation::Status GetCombinedAccessSetupOperationStatus() {
+    return fake_combined_delegate_.status();
   }
 
   void VerifyNotificationAccessGrantedState(AccessStatus expected_status) {
@@ -154,18 +184,31 @@
     EXPECT_EQ(expected_status, manager_->GetAppsAccessStatus());
   }
 
+  void VerifyFeatureSetupRequestSupported(bool expected) {
+    EXPECT_EQ(expected,
+              pref_service_.GetBoolean(prefs::kFeatureSetupRequestSupported));
+    EXPECT_EQ(expected, manager_->GetFeatureSetupRequestSupported());
+  }
+
   bool HasMultideviceFeatureSetupUiBeenDismissed() {
     return manager_->HasMultideviceFeatureSetupUiBeenDismissed();
   }
 
   void DismissSetupRequiredUi() { manager_->DismissSetupRequiredUi(); }
 
-  std::unique_ptr<NotificationAccessSetupOperation> StartSetupOperation() {
-    return manager_->AttemptNotificationSetup(&fake_delegate_);
+  std::unique_ptr<NotificationAccessSetupOperation>
+  StartNotificationSetupOperation() {
+    return manager_->AttemptNotificationSetup(&fake_notification_delegate_);
+  }
+  std::unique_ptr<CombinedAccessSetupOperation> StartCombinedSetupOperation(
+      bool camera_roll,
+      bool notifications) {
+    return manager_->AttemptCombinedFeatureSetup(camera_roll, notifications,
+                                                 &fake_combined_delegate_);
   }
 
-  bool IsSetupOperationInProgress() {
-    return manager_->IsSetupOperationInProgress();
+  bool IsNotificationSetupOperationInProgress() {
+    return manager_->IsNotificationSetupOperationInProgress();
   }
 
   void SetNotificationAccessStatusInternal(AccessStatus status,
@@ -177,6 +220,10 @@
     manager_->SetCameraRollAccessStatusInternal(status);
   }
 
+  void SetFeatureSetupRequestSupportedInternal(bool supported) {
+    manager_->SetFeatureSetupRequestSupportedInternal(supported);
+  }
+
   void SetFeatureStatus(FeatureStatus status) {
     fake_feature_status_provider_->SetStatus(status);
   }
@@ -193,6 +240,10 @@
     return fake_message_sender_->show_notification_access_setup_request_count();
   }
 
+  size_t GetCombinedAccessSetupRequestCallCount() const {
+    return fake_message_sender_->GetFeatureSetupRequestCallCount();
+  }
+
   size_t GetNumObserverCalls() const { return fake_observer_.num_calls(); }
 
   void SetNeedsOneTimeNotificationAccessUpdate(bool needs_update) {
@@ -213,7 +264,8 @@
   TestingPrefServiceSimple pref_service_;
 
   FakeObserver fake_observer_;
-  FakeOperationDelegate fake_delegate_;
+  FakeNotificationAccessSetupOperationDelegate fake_notification_delegate_;
+  FakeCombinedAccessSetupOperationDelegate fake_combined_delegate_;
   std::unique_ptr<multidevice_setup::FakeMultiDeviceSetupClient>
       fake_multidevice_setup_client_;
   std::unique_ptr<FakeFeatureStatusProvider> fake_feature_status_provider_;
@@ -278,7 +330,7 @@
 
   // Cannot start the notification access setup flow if notification and camera
   // roll access have already been granted.
-  auto operation = StartSetupOperation();
+  auto operation = StartNotificationSetupOperation();
   EXPECT_FALSE(operation);
 }
 
@@ -301,7 +353,7 @@
             GetNotificationAccessSetupOperationStatus());
   // Simulate setup operation is in progress. This will trigger a sent
   // request.
-  auto operation = StartSetupOperation();
+  auto operation = StartNotificationSetupOperation();
   EXPECT_TRUE(operation);
   EXPECT_EQ(1u, GetNumShowNotificationAccessSetupRequestCount());
   EXPECT_EQ(NotificationAccessSetupOperation::Status::
@@ -326,7 +378,7 @@
 
   // Start a setup operation with enabled but disconnected status and access
   // not granted.
-  auto operation = StartSetupOperation();
+  auto operation = StartNotificationSetupOperation();
   EXPECT_TRUE(operation);
   EXPECT_EQ(1u, GetNumScheduleConnectionNowCalls());
 
@@ -361,7 +413,7 @@
 
   // Start a setup operation with enabled but disconnected status and access
   // not granted.
-  auto operation = StartSetupOperation();
+  auto operation = StartNotificationSetupOperation();
   EXPECT_TRUE(operation);
   EXPECT_EQ(1u, GetNumScheduleConnectionNowCalls());
 
@@ -399,7 +451,7 @@
 
   // Start a setup operation with enabled and connecting status and access
   // not granted.
-  auto operation = StartSetupOperation();
+  auto operation = StartNotificationSetupOperation();
   EXPECT_TRUE(operation);
 
   // Simulate changing states from connecting to connected.
@@ -431,7 +483,7 @@
 
   // Start a setup operation with enabled and connected status and access
   // not granted.
-  auto operation = StartSetupOperation();
+  auto operation = StartNotificationSetupOperation();
   EXPECT_TRUE(operation);
 
   // Verify that the request message has been sent and our operation status
@@ -459,7 +511,7 @@
   VerifyNotificationAccessGrantedState(AccessStatus::kAvailableButNotGranted);
   VerifyCameraRollAccessGrantedState(AccessStatus::kAvailableButNotGranted);
 
-  auto operation = StartSetupOperation();
+  auto operation = StartNotificationSetupOperation();
   EXPECT_TRUE(operation);
 
   // Simulate a disconnection and expect that status has been updated.
@@ -478,7 +530,7 @@
   VerifyNotificationAccessGrantedState(AccessStatus::kAvailableButNotGranted);
   VerifyCameraRollAccessGrantedState(AccessStatus::kAvailableButNotGranted);
 
-  auto operation = StartSetupOperation();
+  auto operation = StartNotificationSetupOperation();
   EXPECT_TRUE(operation);
 
   EXPECT_EQ(1u, GetNumShowNotificationAccessSetupRequestCount());
@@ -498,7 +550,7 @@
   VerifyNotificationAccessGrantedState(AccessStatus::kAvailableButNotGranted);
   VerifyCameraRollAccessGrantedState(AccessStatus::kAvailableButNotGranted);
 
-  auto operation = StartSetupOperation();
+  auto operation = StartNotificationSetupOperation();
   EXPECT_TRUE(operation);
 
   EXPECT_EQ(1u, GetNumShowNotificationAccessSetupRequestCount());
@@ -678,5 +730,215 @@
   EXPECT_EQ(3u, GetNumObserverCalls());
 }
 
+TEST_F(MultideviceFeatureAccessManagerImplTest,
+       FlipFeatureSetupRequestSupportedOn) {
+  InitializeAccessStatus(AccessStatus::kAvailableButNotGranted,
+                         AccessStatus::kAvailableButNotGranted);
+  VerifyFeatureSetupRequestSupported(false);
+
+  SetFeatureSetupRequestSupportedInternal(true);
+  VerifyFeatureSetupRequestSupported(true);
+  EXPECT_EQ(1u, GetNumObserverCalls());
+}
+
+TEST_F(MultideviceFeatureAccessManagerImplTest,
+       CombinedFeatureSetup_FeatureSetupRequestNotSupported) {
+  InitializeAccessStatus(AccessStatus::kAvailableButNotGranted,
+                         AccessStatus::kAvailableButNotGranted);
+  VerifyNotificationAccessGrantedState(AccessStatus::kAvailableButNotGranted);
+  VerifyCameraRollAccessGrantedState(AccessStatus::kAvailableButNotGranted);
+  VerifyFeatureSetupRequestSupported(false);
+
+  // Cannot start the combined access setup flow if FeatureSetupRequest is not
+  // supported.
+  auto operation =
+      StartCombinedSetupOperation(/*camera_roll=*/true, /*notifications=*/true);
+  EXPECT_FALSE(operation);
+}
+
+TEST_F(MultideviceFeatureAccessManagerImplTest,
+       CombinedFeatureSetup_AllFeaturesGranted_AllFeaturesRequested) {
+  InitializeAccessStatus(AccessStatus::kAccessGranted,
+                         AccessStatus::kAccessGranted,
+                         AccessProhibitedReason::kUnknown,
+                         /*feature_setup_request_supported=*/true);
+  VerifyNotificationAccessGrantedState(AccessStatus::kAccessGranted);
+  VerifyCameraRollAccessGrantedState(AccessStatus::kAccessGranted);
+  VerifyFeatureSetupRequestSupported(true);
+
+  // Cannot start the combined access setup flow if requested feature access
+  // has already been granted.
+  auto operation =
+      StartCombinedSetupOperation(/*camera_roll=*/true, /*notifications=*/true);
+  EXPECT_FALSE(operation);
+}
+
+TEST_F(MultideviceFeatureAccessManagerImplTest,
+       CombinedFeatureSetup_CameraRollGranted_AllFeaturesRequested) {
+  InitializeAccessStatus(AccessStatus::kAvailableButNotGranted,
+                         AccessStatus::kAccessGranted,
+                         AccessProhibitedReason::kUnknown,
+                         /*feature_setup_request_supported=*/true);
+  VerifyNotificationAccessGrantedState(AccessStatus::kAvailableButNotGranted);
+  VerifyCameraRollAccessGrantedState(AccessStatus::kAccessGranted);
+  VerifyFeatureSetupRequestSupported(true);
+
+  // Cannot start the combined access setup flow if requested feature access
+  // has already been granted.
+  auto operation =
+      StartCombinedSetupOperation(/*camera_roll=*/true, /*notifications=*/true);
+  EXPECT_FALSE(operation);
+}
+
+TEST_F(MultideviceFeatureAccessManagerImplTest,
+       CombinedFeatureSetup_NotificationsGranted_AllFeaturesRequested) {
+  InitializeAccessStatus(AccessStatus::kAccessGranted,
+                         AccessStatus::kAvailableButNotGranted,
+                         AccessProhibitedReason::kUnknown,
+                         /*feature_setup_request_supported=*/true);
+  VerifyNotificationAccessGrantedState(AccessStatus::kAccessGranted);
+  VerifyCameraRollAccessGrantedState(AccessStatus::kAvailableButNotGranted);
+  VerifyFeatureSetupRequestSupported(true);
+
+  // Cannot start the combined access setup flow if requested feature access
+  // has already been granted.
+  auto operation =
+      StartCombinedSetupOperation(/*camera_roll=*/true, /*notifications=*/true);
+  EXPECT_FALSE(operation);
+}
+
+TEST_F(MultideviceFeatureAccessManagerImplTest,
+       CombinedFeatureSetup_CameraRollGranted_NotificationsRequested) {
+  InitializeAccessStatus(AccessStatus::kAvailableButNotGranted,
+                         AccessStatus::kAccessGranted,
+                         AccessProhibitedReason::kUnknown,
+                         /*feature_setup_request_supported=*/true);
+  VerifyNotificationAccessGrantedState(AccessStatus::kAvailableButNotGranted);
+  VerifyCameraRollAccessGrantedState(AccessStatus::kAccessGranted);
+  VerifyFeatureSetupRequestSupported(true);
+
+  // Can start the combined access setup flow if requested feature access is not
+  // granted, even if other feature is granted.
+  auto operation = StartCombinedSetupOperation(/*camera_roll=*/false,
+                                               /*notifications=*/true);
+  EXPECT_TRUE(operation);
+}
+
+TEST_F(MultideviceFeatureAccessManagerImplTest,
+       CombinedFeatureSetup_NotificationsGranted_CameraRollRequested) {
+  InitializeAccessStatus(AccessStatus::kAccessGranted,
+                         AccessStatus::kAvailableButNotGranted,
+                         AccessProhibitedReason::kUnknown,
+                         /*feature_setup_request_supported=*/true);
+  VerifyNotificationAccessGrantedState(AccessStatus::kAccessGranted);
+  VerifyCameraRollAccessGrantedState(AccessStatus::kAvailableButNotGranted);
+  VerifyFeatureSetupRequestSupported(true);
+
+  // Can start the combined access setup flow if requested feature access is not
+  // granted, even if other feature is granted.
+  auto operation = StartCombinedSetupOperation(/*camera_roll=*/true,
+                                               /*notifications=*/false);
+  EXPECT_TRUE(operation);
+}
+
+TEST_F(MultideviceFeatureAccessManagerImplTest,
+       CombinedFeatureSetup_FullSetupFromDisconnected) {
+  // Set initial state to disconnected.
+  SetFeatureStatus(FeatureStatus::kEnabledButDisconnected);
+
+  InitializeAccessStatus(AccessStatus::kAvailableButNotGranted,
+                         AccessStatus::kAvailableButNotGranted,
+                         AccessProhibitedReason::kUnknown,
+                         /*feature_setup_request_supported=*/true);
+  VerifyNotificationAccessGrantedState(AccessStatus::kAvailableButNotGranted);
+  VerifyCameraRollAccessGrantedState(AccessStatus::kAvailableButNotGranted);
+  VerifyFeatureSetupRequestSupported(true);
+
+  // Start combined setup operation
+  auto operation = StartCombinedSetupOperation(/*camera_roll=*/true,
+                                               /*notifications=*/true);
+  EXPECT_TRUE(operation);
+  EXPECT_EQ(0u, GetCombinedAccessSetupRequestCallCount());
+  EXPECT_EQ(CombinedAccessSetupOperation::Status::kConnecting,
+            GetCombinedAccessSetupOperationStatus());
+  EXPECT_EQ(1u, GetNumScheduleConnectionNowCalls());
+
+  // Simulate changing state to connecting.
+  SetFeatureStatus(FeatureStatus::kEnabledAndConnecting);
+  EXPECT_EQ(0u, GetCombinedAccessSetupRequestCallCount());
+  EXPECT_EQ(CombinedAccessSetupOperation::Status::kConnecting,
+            GetCombinedAccessSetupOperationStatus());
+  EXPECT_EQ(1u, GetNumScheduleConnectionNowCalls());
+
+  // Simulate changing state to connected.
+  SetFeatureStatus(FeatureStatus::kEnabledAndConnected);
+  EXPECT_EQ(1u, GetCombinedAccessSetupRequestCallCount());
+  EXPECT_EQ(CombinedAccessSetupOperation::Status::
+                kSentMessageToPhoneAndWaitingForResponse,
+            GetCombinedAccessSetupOperationStatus());
+
+  // Simulate Camera Roll being granted on phone.
+  SetCameraRollAccessStatusInternal(AccessStatus::kAccessGranted);
+  VerifyCameraRollAccessGrantedState(AccessStatus::kAccessGranted);
+  EXPECT_EQ(CombinedAccessSetupOperation::Status::
+                kSentMessageToPhoneAndWaitingForResponse,
+            GetCombinedAccessSetupOperationStatus());
+
+  // Simulate Notifications being granted on phone.
+  SetNotificationAccessStatusInternal(AccessStatus::kAccessGranted,
+                                      AccessProhibitedReason::kUnknown);
+  VerifyNotificationAccessGrantedState(AccessStatus::kAccessGranted);
+  EXPECT_EQ(CombinedAccessSetupOperation::Status::kCompletedSuccessfully,
+            GetCombinedAccessSetupOperationStatus());
+}
+
+TEST_F(MultideviceFeatureAccessManagerImplTest,
+       CombinedFeatureSetup_SimulateTimeout) {
+  // Set initial state to connecting.
+  SetFeatureStatus(FeatureStatus::kEnabledAndConnecting);
+
+  InitializeAccessStatus(AccessStatus::kAvailableButNotGranted,
+                         AccessStatus::kAvailableButNotGranted,
+                         AccessProhibitedReason::kUnknown,
+                         /*feature_setup_request_supported=*/true);
+  VerifyNotificationAccessGrantedState(AccessStatus::kAvailableButNotGranted);
+  VerifyCameraRollAccessGrantedState(AccessStatus::kAvailableButNotGranted);
+  VerifyFeatureSetupRequestSupported(true);
+
+  // Start combined setup operation
+  auto operation = StartCombinedSetupOperation(/*camera_roll=*/true,
+                                               /*notifications=*/true);
+  EXPECT_TRUE(operation);
+
+  // Simulate a disconnection and expect that status has been updated.
+  SetFeatureStatus(FeatureStatus::kEnabledButDisconnected);
+  EXPECT_EQ(CombinedAccessSetupOperation::Status::kTimedOutConnecting,
+            GetCombinedAccessSetupOperationStatus());
+}
+
+TEST_F(MultideviceFeatureAccessManagerImplTest,
+       CombinedFeatureSetup_SimulateDisconnect) {
+  // Set initial state to connected.
+  SetFeatureStatus(FeatureStatus::kEnabledAndConnected);
+
+  InitializeAccessStatus(AccessStatus::kAvailableButNotGranted,
+                         AccessStatus::kAvailableButNotGranted,
+                         AccessProhibitedReason::kUnknown,
+                         /*feature_setup_request_supported=*/true);
+  VerifyNotificationAccessGrantedState(AccessStatus::kAvailableButNotGranted);
+  VerifyCameraRollAccessGrantedState(AccessStatus::kAvailableButNotGranted);
+  VerifyFeatureSetupRequestSupported(true);
+
+  // Start combined setup operation
+  auto operation = StartCombinedSetupOperation(/*camera_roll=*/true,
+                                               /*notifications=*/true);
+  EXPECT_TRUE(operation);
+
+  // Simulate a disconnection and expect that status has been updated.
+  SetFeatureStatus(FeatureStatus::kEnabledButDisconnected);
+  EXPECT_EQ(CombinedAccessSetupOperation::Status::kConnectionDisconnected,
+            GetCombinedAccessSetupOperationStatus());
+}
+
 }  // namespace phonehub
 }  // namespace ash
diff --git a/ash/components/phonehub/notification_access_setup_operation.cc b/ash/components/phonehub/notification_access_setup_operation.cc
index 1b4e914..e1d86c2 100644
--- a/ash/components/phonehub/notification_access_setup_operation.cc
+++ b/ash/components/phonehub/notification_access_setup_operation.cc
@@ -56,7 +56,8 @@
   std::move(destructor_callback_).Run();
 }
 
-void NotificationAccessSetupOperation::NotifyStatusChanged(Status new_status) {
+void NotificationAccessSetupOperation::NotifyNotificationStatusChanged(
+    Status new_status) {
   base::UmaHistogramEnumeration("PhoneHub.NotificationAccessSetup.AllStatuses",
                                 new_status);
   if (new_status == Status::kCompletedSuccessfully) {
@@ -68,7 +69,7 @@
   }
   current_status_ = new_status;
 
-  delegate_->OnStatusChange(new_status);
+  delegate_->OnNotificationStatusChange(new_status);
 }
 
 std::ostream& operator<<(std::ostream& stream,
diff --git a/ash/components/phonehub/notification_access_setup_operation.h b/ash/components/phonehub/notification_access_setup_operation.h
index efa59d3..b1f0773 100644
--- a/ash/components/phonehub/notification_access_setup_operation.h
+++ b/ash/components/phonehub/notification_access_setup_operation.h
@@ -69,7 +69,7 @@
     virtual ~Delegate() = default;
 
     // Called when status of the setup flow has changed.
-    virtual void OnStatusChange(Status new_status) = 0;
+    virtual void OnNotificationStatusChange(Status new_status) = 0;
   };
 
   NotificationAccessSetupOperation(const NotificationAccessSetupOperation&) =
@@ -84,7 +84,7 @@
   NotificationAccessSetupOperation(Delegate* delegate,
                                    base::OnceClosure destructor_callback);
 
-  void NotifyStatusChanged(Status new_status);
+  void NotifyNotificationStatusChanged(Status new_status);
 
   absl::optional<Status> current_status_;
   const base::TimeTicks start_timestamp_ = base::TimeTicks::Now();
diff --git a/ash/components/phonehub/phone_status_processor.cc b/ash/components/phonehub/phone_status_processor.cc
index 4651daf6..7a7a649 100644
--- a/ash/components/phonehub/phone_status_processor.cc
+++ b/ash/components/phonehub/phone_status_processor.cc
@@ -284,6 +284,10 @@
     recent_apps_interaction_handler_->set_user_states(
         GetUserStates(phone_properties.user_states()));
   }
+
+  multidevice_feature_access_manager_->SetFeatureSetupRequestSupportedInternal(
+      phone_properties.feature_setup_config()
+          .feature_setup_request_supported());
 }
 
 void PhoneStatusProcessor::MaybeSetPhoneModelName(
diff --git a/ash/components/phonehub/phone_status_processor_unittest.cc b/ash/components/phonehub/phone_status_processor_unittest.cc
index 86fc413..a42ccb4 100644
--- a/ash/components/phonehub/phone_status_processor_unittest.cc
+++ b/ash/components/phonehub/phone_status_processor_unittest.cc
@@ -180,6 +180,9 @@
   proto::CameraRollAccessState* access_state =
       expected_phone_properties->mutable_camera_roll_access_state();
   access_state->set_feature_enabled(true);
+  proto::FeatureSetupConfig* feature_setup_config =
+      expected_phone_properties->mutable_feature_setup_config();
+  feature_setup_config->set_feature_setup_request_supported(true);
 
   expected_phone_properties->add_user_states();
   proto::UserState* mutable_user_state =
@@ -215,6 +218,8 @@
   EXPECT_EQ(
       MultideviceFeatureAccessManager::AccessStatus::kAccessGranted,
       fake_multidevice_feature_access_manager_->GetCameraRollAccessStatus());
+  EXPECT_TRUE(fake_multidevice_feature_access_manager_
+                  ->GetFeatureSetupRequestSupported());
   EXPECT_EQ(ScreenLockManager::LockStatus::kUnknown,
             fake_screen_lock_manager_->GetLockStatus());
 
@@ -273,6 +278,9 @@
   proto::CameraRollAccessState* access_state =
       expected_phone_properties->mutable_camera_roll_access_state();
   access_state->set_feature_enabled(false);
+  proto::FeatureSetupConfig* feature_setup_config =
+      expected_phone_properties->mutable_feature_setup_config();
+  feature_setup_config->set_feature_setup_request_supported(false);
 
   expected_phone_properties->add_user_states();
   proto::UserState* mutable_user_state =
@@ -307,6 +315,8 @@
   EXPECT_EQ(
       MultideviceFeatureAccessManager::AccessStatus::kAvailableButNotGranted,
       fake_multidevice_feature_access_manager_->GetCameraRollAccessStatus());
+  EXPECT_FALSE(fake_multidevice_feature_access_manager_
+                   ->GetFeatureSetupRequestSupported());
   EXPECT_EQ(ScreenLockManager::LockStatus::kLockedOff,
             fake_screen_lock_manager_->GetLockStatus());
 
diff --git a/ash/components/phonehub/pref_names.cc b/ash/components/phonehub/pref_names.cc
index 1ff0e5a..67fbab8 100644
--- a/ash/components/phonehub/pref_names.cc
+++ b/ash/components/phonehub/pref_names.cc
@@ -65,6 +65,11 @@
 // pref stores the vector value associated with Notification::AppMetadata.
 const char kRecentAppsHistory[] = "cros.phonehub.recent_apps_history";
 
+// Whether the phone supports setting up multiple features at the same time
+// using the FeatureSetupRequest.
+const char kFeatureSetupRequestSupported[] =
+    "cros.phonehub.feature_setup_request_supported";
+
 }  // namespace prefs
 }  // namespace phonehub
 }  // namespace ash
diff --git a/ash/components/phonehub/pref_names.h b/ash/components/phonehub/pref_names.h
index 1e2a49de..b4ae8bbc 100644
--- a/ash/components/phonehub/pref_names.h
+++ b/ash/components/phonehub/pref_names.h
@@ -18,6 +18,7 @@
 extern const char kNeedsOneTimeNotificationAccessUpdate[];
 extern const char kScreenLockStatus[];
 extern const char kRecentAppsHistory[];
+extern const char kFeatureSetupRequestSupported[];
 
 }  // namespace prefs
 }  // namespace phonehub
diff --git a/ash/public/cpp/desk_template.cc b/ash/public/cpp/desk_template.cc
index bd23c3b7..bb4874d 100644
--- a/ash/public/cpp/desk_template.cc
+++ b/ash/public/cpp/desk_template.cc
@@ -35,9 +35,6 @@
     case AppType::LACROS:
       return false;
     case AppType::ARC_APP:
-      if (!features::AreDesksTemplatesEnabled())
-        return false;
-      break;
     case AppType::BROWSER:
     case AppType::CHROME_APP:
     case AppType::SYSTEM_APP:
diff --git a/ash/shelf/shelf_layout_manager_unittest.cc b/ash/shelf/shelf_layout_manager_unittest.cc
index 18a18db..e1110e43 100644
--- a/ash/shelf/shelf_layout_manager_unittest.cc
+++ b/ash/shelf/shelf_layout_manager_unittest.cc
@@ -59,6 +59,7 @@
 #include "ash/system/status_area_widget_test_helper.h"
 #include "ash/system/unified/unified_system_tray.h"
 #include "ash/test/ash_test_base.h"
+#include "ash/test/test_widget_builder.h"
 #include "ash/wallpaper/wallpaper_controller_impl.h"
 #include "ash/wm/lock_state_controller.h"
 #include "ash/wm/overview/overview_controller.h"
@@ -1310,6 +1311,29 @@
   EXPECT_EQ(SHELF_AUTO_HIDE_HIDDEN, shelf_2->GetAutoHideState());
 }
 
+TEST_F(ShelfLayoutManagerTest, FullscreenWidgetHidesShelf) {
+  Shelf* shelf = GetPrimaryShelf();
+  // Create a normal window.
+  views::Widget* widget = TestWidgetBuilder()
+                              .SetBounds(gfx::Rect(11, 22, 300, 400))
+                              .BuildOwnedByNativeWidget();
+  ASSERT_FALSE(widget->IsFullscreen());
+
+  // Shelf defaults to visible.
+  EXPECT_EQ(SHELF_VISIBLE, shelf->GetVisibilityState());
+
+  // Fullscreen window hides it.
+  widget->SetFullscreen(true);
+  EXPECT_EQ(SHELF_HIDDEN, shelf->GetVisibilityState());
+
+  // Restoring the window restores it.
+  widget->Restore();
+  EXPECT_EQ(SHELF_VISIBLE, shelf->GetVisibilityState());
+
+  // Clean up.
+  widget->Close();
+}
+
 // Tests that the shelf is only hidden for a fullscreen window at the front and
 // toggles visibility when another window is activated.
 TEST_F(ShelfLayoutManagerTest, FullscreenWindowInFrontHidesShelf) {
diff --git a/ash/shelf/shelf_unittest.cc b/ash/shelf/shelf_unittest.cc
index 76dc61b..5e34c9a4 100644
--- a/ash/shelf/shelf_unittest.cc
+++ b/ash/shelf/shelf_unittest.cc
@@ -25,6 +25,8 @@
 #include "base/strings/stringprintf.h"
 #include "base/threading/thread_task_runner_handle.h"
 #include "components/session_manager/session_manager_types.h"
+#include "ui/aura/client/aura_constants.h"
+#include "ui/wm/core/window_util.h"
 
 namespace ash {
 namespace {
@@ -110,6 +112,34 @@
   shelf_model()->RemoveItemAt(index);
 }
 
+// Various assertions around auto-hide behavior.
+TEST_F(ShelfTest, ToggleAutoHide) {
+  std::unique_ptr<aura::Window> window =
+      std::make_unique<aura::Window>(nullptr);
+  window->SetProperty(aura::client::kShowStateKey, ui::SHOW_STATE_NORMAL);
+  window->SetType(aura::client::WINDOW_TYPE_NORMAL);
+  window->Init(ui::LAYER_TEXTURED);
+  ParentWindowInPrimaryRootWindow(window.get());
+  window->Show();
+  wm::ActivateWindow(window.get());
+
+  Shelf* shelf = GetPrimaryShelf();
+  shelf->SetAutoHideBehavior(ShelfAutoHideBehavior::kAlways);
+  EXPECT_EQ(ShelfAutoHideBehavior::kAlways, shelf->auto_hide_behavior());
+
+  shelf->SetAutoHideBehavior(ShelfAutoHideBehavior::kNever);
+  EXPECT_EQ(ShelfAutoHideBehavior::kNever, shelf->auto_hide_behavior());
+
+  window->SetProperty(aura::client::kShowStateKey, ui::SHOW_STATE_MAXIMIZED);
+  EXPECT_EQ(ShelfAutoHideBehavior::kNever, shelf->auto_hide_behavior());
+
+  shelf->SetAutoHideBehavior(ShelfAutoHideBehavior::kAlways);
+  EXPECT_EQ(ShelfAutoHideBehavior::kAlways, shelf->auto_hide_behavior());
+
+  shelf->SetAutoHideBehavior(ShelfAutoHideBehavior::kNever);
+  EXPECT_EQ(ShelfAutoHideBehavior::kNever, shelf->auto_hide_behavior());
+}
+
 // Tests if shelf is hidden on secondary display after the primary display is
 // changed.
 TEST_F(ShelfTest, ShelfHiddenOnScreenOnSecondaryDisplay) {
diff --git a/ash/shell.cc b/ash/shell.cc
index 78ad21c..1a9eccb 100644
--- a/ash/shell.cc
+++ b/ash/shell.cc
@@ -115,6 +115,7 @@
 #include "ash/system/brightness_control_delegate.h"
 #include "ash/system/caps_lock_notification_controller.h"
 #include "ash/system/firmware_update/firmware_update_notification_controller.h"
+#include "ash/system/geolocation/geolocation_controller.h"
 #include "ash/system/hps/hps_orientation_controller.h"
 #include "ash/system/keyboard_brightness/keyboard_brightness_controller.h"
 #include "ash/system/keyboard_brightness_control_delegate.h"
@@ -835,14 +836,17 @@
   // Removes itself as an observer of |pref_service_|.
   shelf_controller_.reset();
 
-  // NightLightControllerImpl depends on the PrefService as well as the window
-  // tree host manager, and must be destructed before them. crbug.com/724231.
+  // NightLightControllerImpl depends on the PrefService, the window tree host
+  // manager, and `geolocation_controller_`, so it must be destructed before
+  // them. crbug.com/724231.
   night_light_controller_ = nullptr;
   // Similarly for DockedMagnifierController.
   docked_magnifier_controller_ = nullptr;
   // Similarly for PrivacyScreenController.
   privacy_screen_controller_ = nullptr;
 
+  geolocation_controller_.reset();
+
   // NearbyShareDelegateImpl must be destroyed before SessionController and
   // NearbyShareControllerImpl.
   nearby_share_delegate_.reset();
@@ -1070,8 +1074,12 @@
   ui::ColorProviderManager::Get().AppendColorProviderInitializer(
       base::BindRepeating(AddAshColorMixer));
 
-  // Night Light depends on the display manager, the display color manager, and
-  // aura::Env, so initialize it after all have been initialized.
+  geolocation_controller_ = std::make_unique<GeolocationController>(
+      shell_delegate_->GetGeolocationUrlLoaderFactory());
+
+  // Night Light depends on the display manager, the display color manager,
+  // aura::Env, and geolocation controller, so initialize it after all have
+  // been initialized.
   night_light_controller_ = std::make_unique<NightLightControllerImpl>();
 
   // Privacy Screen depends on the display manager, so initialize it after
diff --git a/ash/shell.h b/ash/shell.h
index bfad360..4a24de05 100644
--- a/ash/shell.h
+++ b/ash/shell.h
@@ -129,6 +129,7 @@
 class FocusCycler;
 class FrameThrottlingController;
 class FullscreenMagnifierController;
+class GeolocationController;
 class HighContrastController;
 class HighlighterController;
 class HoldingSpaceController;
@@ -429,6 +430,9 @@
   FullscreenMagnifierController* fullscreen_magnifier_controller() {
     return fullscreen_magnifier_controller_.get();
   }
+  GeolocationController* geolocation_controller() {
+    return geolocation_controller_.get();
+  }
   HighlighterController* highlighter_controller() {
     return highlighter_controller_.get();
   }
@@ -490,9 +494,6 @@
   NightLightControllerImpl* night_light_controller() {
     return night_light_controller_.get();
   }
-  PrivacyScreenController* privacy_screen_controller() {
-    return privacy_screen_controller_.get();
-  }
   OverlayEventFilter* overlay_filter() { return overlay_filter_.get(); }
   ParentAccessController* parent_access_controller() {
     return parent_access_controller_.get();
@@ -512,6 +513,9 @@
   PowerEventObserver* power_event_observer() {
     return power_event_observer_.get();
   }
+  PrivacyScreenController* privacy_screen_controller() {
+    return privacy_screen_controller_.get();
+  }
   quick_pair::Mediator* quick_pair_mediator() {
     return quick_pair_mediator_.get();
   }
@@ -768,6 +772,7 @@
       firmware_update_notification_controller_;
   std::unique_ptr<FocusCycler> focus_cycler_;
   std::unique_ptr<FloatController> float_controller_;
+  std::unique_ptr<GeolocationController> geolocation_controller_;
   std::unique_ptr<HoldingSpaceController> holding_space_controller_;
   std::unique_ptr<HpsNotifyController> hps_notify_controller_;
   std::unique_ptr<HpsOrientationController> hps_orientation_controller_;
diff --git a/ash/shell_delegate.h b/ash/shell_delegate.h
index d1e9423..5f4da634 100644
--- a/ash/shell_delegate.h
+++ b/ash/shell_delegate.h
@@ -69,7 +69,7 @@
   // Returns the geolocation loader factory used to initialize geolocation
   // provider.
   virtual scoped_refptr<network::SharedURLLoaderFactory>
-  GetGeolocationSharedURLLoaderFactory() const = 0;
+  GetGeolocationUrlLoaderFactory() const = 0;
 
   // Check whether the current tab of the browser window can go back.
   virtual bool CanGoBack(gfx::NativeWindow window) const = 0;
diff --git a/ash/shell_unittest.cc b/ash/shell_unittest.cc
index 2076fbf..9b9f3d7 100644
--- a/ash/shell_unittest.cc
+++ b/ash/shell_unittest.cc
@@ -34,13 +34,11 @@
 #include "ash/wallpaper/wallpaper_widget_controller.h"
 #include "ash/wm/desks/desks_util.h"
 #include "ash/wm/overview/overview_controller.h"
-#include "ash/wm/window_util.h"
 #include "base/command_line.h"
 #include "base/containers/flat_set.h"
 #include "base/strings/utf_string_conversions.h"
 #include "base/threading/thread_task_runner_handle.h"
 #include "components/account_id/account_id.h"
-#include "ui/aura/client/aura_constants.h"
 #include "ui/aura/env.h"
 #include "ui/aura/window.h"
 #include "ui/aura/window_event_dispatcher.h"
@@ -429,65 +427,6 @@
   widget->Close();
 }
 
-TEST_F(ShellTest, FullscreenWindowHidesShelf) {
-  ExpectAllContainers();
-
-  // Create a normal window.  It is not maximized.
-  views::Widget* widget = TestWidgetBuilder()
-                              .SetBounds(gfx::Rect(11, 22, 300, 400))
-                              .BuildOwnedByNativeWidget();
-  EXPECT_FALSE(widget->IsMaximized());
-
-  // Shelf defaults to visible.
-  EXPECT_EQ(SHELF_VISIBLE, Shell::GetPrimaryRootWindowController()
-                               ->GetShelfLayoutManager()
-                               ->visibility_state());
-
-  // Fullscreen window hides it.
-  widget->SetFullscreen(true);
-  EXPECT_EQ(SHELF_HIDDEN, Shell::GetPrimaryRootWindowController()
-                              ->GetShelfLayoutManager()
-                              ->visibility_state());
-
-  // Restoring the window restores it.
-  widget->Restore();
-  EXPECT_EQ(SHELF_VISIBLE, Shell::GetPrimaryRootWindowController()
-                               ->GetShelfLayoutManager()
-                               ->visibility_state());
-
-  // Clean up.
-  widget->Close();
-}
-
-// Various assertions around auto-hide behavior.
-// TODO(jamescook): Move this to ShelfTest.
-TEST_F(ShellTest, ToggleAutoHide) {
-  std::unique_ptr<aura::Window> window =
-      std::make_unique<aura::Window>(nullptr);
-  window->SetProperty(aura::client::kShowStateKey, ui::SHOW_STATE_NORMAL);
-  window->SetType(aura::client::WINDOW_TYPE_NORMAL);
-  window->Init(ui::LAYER_TEXTURED);
-  ParentWindowInPrimaryRootWindow(window.get());
-  window->Show();
-  wm::ActivateWindow(window.get());
-
-  Shelf* shelf = GetPrimaryShelf();
-  shelf->SetAutoHideBehavior(ShelfAutoHideBehavior::kAlways);
-  EXPECT_EQ(ShelfAutoHideBehavior::kAlways, shelf->auto_hide_behavior());
-
-  shelf->SetAutoHideBehavior(ShelfAutoHideBehavior::kNever);
-  EXPECT_EQ(ShelfAutoHideBehavior::kNever, shelf->auto_hide_behavior());
-
-  window->SetProperty(aura::client::kShowStateKey, ui::SHOW_STATE_MAXIMIZED);
-  EXPECT_EQ(ShelfAutoHideBehavior::kNever, shelf->auto_hide_behavior());
-
-  shelf->SetAutoHideBehavior(ShelfAutoHideBehavior::kAlways);
-  EXPECT_EQ(ShelfAutoHideBehavior::kAlways, shelf->auto_hide_behavior());
-
-  shelf->SetAutoHideBehavior(ShelfAutoHideBehavior::kNever);
-  EXPECT_EQ(ShelfAutoHideBehavior::kNever, shelf->auto_hide_behavior());
-}
-
 // Tests that the cursor-filter is ahead of the drag-drop controller in the
 // pre-target list.
 TEST_F(ShellTest, TestPreTargetHandlerOrder) {
diff --git a/ash/style/ash_color_provider.cc b/ash/style/ash_color_provider.cc
index 93332df3..cb18404 100644
--- a/ash/style/ash_color_provider.cc
+++ b/ash/style/ash_color_provider.cc
@@ -280,10 +280,8 @@
     return false;
   }
 
-  // Keep it at dark mode if it is not in an active user session or
-  // kDarkLightMode feature is not enabled.
-  // TODO(minch): Besides OOBE, make LIGHT as the color mode for other
-  // non-active user session as well while enabling D/L feature.
+  // Keep the color mode as DARK in login screen or when dark/light mode feature
+  // is not enabled.
   if (!active_user_pref_service_ || !features::IsDarkLightModeEnabled())
     return true;
   return active_user_pref_service_->GetBoolean(prefs::kDarkModeEnabled);
diff --git a/ash/system/bluetooth/bluetooth_feature_pod_controller.cc b/ash/system/bluetooth/bluetooth_feature_pod_controller.cc
index 8645ea5..06c5b0ff 100644
--- a/ash/system/bluetooth/bluetooth_feature_pod_controller.cc
+++ b/ash/system/bluetooth/bluetooth_feature_pod_controller.cc
@@ -81,7 +81,10 @@
     const {
   return first_connected_device_.has_value() &&
          first_connected_device_.value().battery_info &&
-         first_connected_device_.value().battery_info->default_properties;
+         (first_connected_device_.value().battery_info->default_properties ||
+          first_connected_device_.value().battery_info->left_bud_info ||
+          first_connected_device_.value().battery_info->right_bud_info ||
+          first_connected_device_.value().battery_info->case_info);
 }
 
 const gfx::VectorIcon& BluetoothFeaturePodController::ComputeButtonIcon()
@@ -103,6 +106,31 @@
   return l10n_util::GetStringUTF16(IDS_ASH_STATUS_TRAY_BLUETOOTH);
 }
 
+int BluetoothFeaturePodController::
+    GetFirstConnectedDeviceBatteryLevelForDisplay() const {
+  // If there are any multiple battery details, we should prioritize showing
+  // them over the default battery in order to match the Quick Settings
+  // Bluetooth sub-page battery details shown. Android only shows the left bud
+  // if there are multiple batteries, so we match that here, and if it doesn't
+  // exist, we prioritize the right bud battery, then the case battery, if they
+  // exist over the default battery in order to match any detailed battery
+  // shown on the sub-page.
+  if (first_connected_device_.value().battery_info->left_bud_info)
+    return first_connected_device_.value()
+        .battery_info->left_bud_info->battery_percentage;
+
+  if (first_connected_device_.value().battery_info->right_bud_info)
+    return first_connected_device_.value()
+        .battery_info->right_bud_info->battery_percentage;
+
+  if (first_connected_device_.value().battery_info->case_info)
+    return first_connected_device_.value()
+        .battery_info->case_info->battery_percentage;
+
+  return first_connected_device_.value()
+      .battery_info->default_properties->battery_percentage;
+}
+
 std::u16string BluetoothFeaturePodController::ComputeButtonSubLabel() const {
   if (!button_->IsToggled())
     return l10n_util::GetStringUTF16(
@@ -117,8 +145,7 @@
       return l10n_util::GetStringFUTF16(
           IDS_ASH_STATUS_TRAY_BLUETOOTH_DEVICE_BATTERY_PERCENTAGE_LABEL,
           base::NumberToString16(
-              first_connected_device_.value()
-                  .battery_info->default_properties->battery_percentage));
+              GetFirstConnectedDeviceBatteryLevelForDisplay()));
     }
     return l10n_util::GetStringUTF16(
         IDS_ASH_STATUS_TRAY_BLUETOOTH_DEVICE_CONNECTED_LABEL);
@@ -140,8 +167,7 @@
           IDS_ASH_STATUS_TRAY_BLUETOOTH_DEVICE_CONNECTED_WITH_BATTERY_TOOLTIP,
           first_connected_device_.value().device_name,
           base::NumberToString16(
-              first_connected_device_.value()
-                  .battery_info->default_properties->battery_percentage));
+              GetFirstConnectedDeviceBatteryLevelForDisplay()));
     }
     return l10n_util::GetStringFUTF16(
         IDS_ASH_STATUS_TRAY_BLUETOOTH_DEVICE_CONNECTED_TOOLTIP,
diff --git a/ash/system/bluetooth/bluetooth_feature_pod_controller.h b/ash/system/bluetooth/bluetooth_feature_pod_controller.h
index 7537d5c..b1526ce 100644
--- a/ash/system/bluetooth/bluetooth_feature_pod_controller.h
+++ b/ash/system/bluetooth/bluetooth_feature_pod_controller.h
@@ -53,6 +53,7 @@
   };
 
   bool DoesFirstConnectedDeviceHaveBatteryInfo() const;
+  int GetFirstConnectedDeviceBatteryLevelForDisplay() const;
 
   const gfx::VectorIcon& ComputeButtonIcon() const;
   std::u16string ComputeButtonLabel() const;
diff --git a/ash/system/bluetooth/bluetooth_feature_pod_controller_unittest.cc b/ash/system/bluetooth/bluetooth_feature_pod_controller_unittest.cc
index 5174bc7..25b30ff 100644
--- a/ash/system/bluetooth/bluetooth_feature_pod_controller_unittest.cc
+++ b/ash/system/bluetooth/bluetooth_feature_pod_controller_unittest.cc
@@ -46,6 +46,9 @@
 const char* kDeviceNickname = "fancy squares";
 const char* kDevicePublicName = "Rubik's Cube";
 constexpr uint8_t kBatteryPercentage = 27;
+constexpr uint8_t kLeftBudBatteryPercentage = 23;
+constexpr uint8_t kRightBudBatteryPercentage = 11;
+constexpr uint8_t kCaseBatteryPercentage = 77;
 
 // How many devices to "pair" for tests that require multiple connected devices.
 constexpr int kMultipleDeviceCount = 3;
@@ -80,6 +83,31 @@
     return battery_info;
   }
 
+  DeviceBatteryInfoPtr CreateMultipleBatteryInfo(
+      absl::optional<int> left_bud_battery,
+      absl::optional<int> case_battery,
+      absl::optional<int> right_bud_battery) {
+    DeviceBatteryInfoPtr battery_info = DeviceBatteryInfo::New();
+
+    if (left_bud_battery) {
+      battery_info->left_bud_info = BatteryProperties::New();
+      battery_info->left_bud_info->battery_percentage =
+          left_bud_battery.value();
+    }
+
+    if (case_battery) {
+      battery_info->case_info = BatteryProperties::New();
+      battery_info->case_info->battery_percentage = case_battery.value();
+    }
+
+    if (right_bud_battery) {
+      battery_info->right_bud_info = BatteryProperties::New();
+      battery_info->right_bud_info->battery_percentage =
+          right_bud_battery.value();
+    }
+    return battery_info;
+  }
+
   void ExpectBluetoothDetailedViewFocused() {
     EXPECT_TRUE(tray_view()->detailed_view());
     const FeaturePodIconButton::Views& children =
@@ -300,6 +328,48 @@
 }
 
 TEST_F(BluetoothFeaturePodControllerTest,
+       HasCorrectMetadataWithOneDevice_MultipleBatteries) {
+  SetSystemState(BluetoothSystemState::kEnabled);
+
+  const std::u16string public_name = base::ASCIIToUTF16(kDevicePublicName);
+  auto paired_device = PairedBluetoothDeviceProperties::New();
+  paired_device->device_properties = BluetoothDeviceProperties::New();
+  paired_device->device_properties->public_name = public_name;
+  paired_device->device_properties->connection_state =
+      DeviceConnectionState::kConnected;
+  paired_device->device_properties->battery_info =
+      CreateMultipleBatteryInfo(/*left_bud_battery=*/kLeftBudBatteryPercentage,
+                                /*case_battery=*/kCaseBatteryPercentage,
+                                /*right_battery=*/kRightBudBatteryPercentage);
+  SetConnectedDevice(paired_device);
+
+  const ash::FeaturePodLabelButton* label_button = feature_pod_label_button();
+  EXPECT_EQ(l10n_util::GetStringFUTF16(
+                IDS_ASH_STATUS_TRAY_BLUETOOTH_DEVICE_BATTERY_PERCENTAGE_LABEL,
+                base::NumberToString16(kLeftBudBatteryPercentage)),
+            label_button->GetSubLabelText());
+
+  paired_device->device_properties->battery_info =
+      CreateMultipleBatteryInfo(/*left_bud_battery=*/absl::nullopt,
+                                /*case_battery=*/kCaseBatteryPercentage,
+                                /*right_battery=*/kRightBudBatteryPercentage);
+  SetConnectedDevice(paired_device);
+  EXPECT_EQ(l10n_util::GetStringFUTF16(
+                IDS_ASH_STATUS_TRAY_BLUETOOTH_DEVICE_BATTERY_PERCENTAGE_LABEL,
+                base::NumberToString16(kRightBudBatteryPercentage)),
+            label_button->GetSubLabelText());
+
+  paired_device->device_properties->battery_info = CreateMultipleBatteryInfo(
+      /*left_bud_battery=*/absl::nullopt,
+      /*case_battery=*/kCaseBatteryPercentage, /*right_battery=*/absl::nullopt);
+  SetConnectedDevice(paired_device);
+  EXPECT_EQ(l10n_util::GetStringFUTF16(
+                IDS_ASH_STATUS_TRAY_BLUETOOTH_DEVICE_BATTERY_PERCENTAGE_LABEL,
+                base::NumberToString16(kCaseBatteryPercentage)),
+            label_button->GetSubLabelText());
+}
+
+TEST_F(BluetoothFeaturePodControllerTest,
        HasCorrectMetadataWithMultipleDevice) {
   SetSystemState(BluetoothSystemState::kEnabled);
 
diff --git a/ash/system/eche/eche_tray.cc b/ash/system/eche/eche_tray.cc
index 7451be5..1eaa9207 100644
--- a/ash/system/eche/eche_tray.cc
+++ b/ash/system/eche/eche_tray.cc
@@ -7,7 +7,7 @@
 #include <algorithm>
 
 #include "ash/accessibility/accessibility_controller_impl.h"
-#include "ash/constants/ash_features.h"
+#include "ash/components/multidevice/logging/logging.h"
 #include "ash/public/cpp/ash_web_view.h"
 #include "ash/public/cpp/ash_web_view_factory.h"
 #include "ash/public/cpp/shell_window_ids.h"
@@ -27,6 +27,7 @@
 #include "ash/system/tray/tray_container.h"
 #include "ash/system/tray/tray_popup_utils.h"
 #include "ash/system/tray/tray_utils.h"
+#include "ash/webui/eche_app_ui/mojom/eche_app.mojom.h"
 #include "base/bind.h"
 #include "base/callback_forward.h"
 #include "components/account_id/account_id.h"
@@ -62,9 +63,9 @@
 constexpr int kIconSize = 22;
 
 constexpr int kHeaderHeight = 40;
-constexpr int kHeaderHorizontalInteriorMargins = 12;
+constexpr int kHeaderHorizontalInteriorMargins = 0;
 constexpr gfx::Insets kHeaderDefaultSpacing =
-    gfx::Insets(/*vertical=*/0, /*horizontal=*/8);
+    gfx::Insets(/*vertical=*/0, /*horizontal=*/6);
 
 constexpr gfx::Insets kBubblePadding(/*vertical=*/8, /*horizontal=*/8);
 
@@ -195,6 +196,23 @@
   HideBubbleWithView(bubble_view);
 }
 
+void EcheTray::OnStreamStatusChanged(eche_app::mojom::StreamStatus status) {
+  switch (status) {
+    case eche_app::mojom::StreamStatus::kStreamStatusStarted:
+      ShowBubble();
+      break;
+    case eche_app::mojom::StreamStatus::kStreamStatusStopped:
+      PurgeAndClose();
+      break;
+    case eche_app::mojom::StreamStatus::kStreamStatusInitializing:
+      // Ignores initializing stream status.
+      break;
+    case eche_app::mojom::StreamStatus::kStreamStatusUnknown:
+      PA_LOG(WARNING) << "Unexpected stream status";
+      break;
+  }
+}
+
 void EcheTray::OnLockStateChanged(bool locked) {
   if (bubble_ && locked)
     PurgeAndClose();
@@ -330,7 +348,7 @@
       ->SetInteriorMargin(
           gfx::Insets(/*vertical=*/0,
                       /*horizontal=*/kHeaderHorizontalInteriorMargins))
-      .SetCollapseMargins(true)
+      .SetCollapseMargins(false)
       .SetMinimumCrossAxisSize(kHeaderHeight)
       .SetDefault(views::kMarginsKey, kHeaderDefaultSpacing)
       .SetCrossAxisAlignment(views::LayoutAlignment::kCenter);
diff --git a/ash/system/eche/eche_tray.h b/ash/system/eche/eche_tray.h
index 9941cfa..795e86c0 100644
--- a/ash/system/eche/eche_tray.h
+++ b/ash/system/eche/eche_tray.h
@@ -12,6 +12,7 @@
 #include "ash/session/session_controller_impl.h"
 #include "ash/system/eche/eche_icon_loading_indicator_view.h"
 #include "ash/system/tray/tray_background_view.h"
+#include "ash/webui/eche_app_ui/mojom/eche_app.mojom.h"
 #include "base/gtest_prod_util.h"
 #include "components/session_manager/session_manager_types.h"
 #include "ui/views/controls/button/button.h"
@@ -97,6 +98,12 @@
 
   void HideBubble();
 
+  // Receives the `status` change when the video streaming is started or
+  // stopped. Controls the bubble widget based on the different `status`
+  // changes. There are two cases: 1. Shows the bubble when the streaming is
+  // started. 2. Purges and closes the bubble when the streaming is stopped.
+  void OnStreamStatusChanged(eche_app::mojom::StreamStatus status);
+
   // Set up the params and init the bubble.
   // Note: This function makes the bubble active and makes the
   // TrayBackgroundView's background inkdrop activate.
diff --git a/ash/system/eche/eche_tray_unittest.cc b/ash/system/eche/eche_tray_unittest.cc
index 688ca8b..d6586f8 100644
--- a/ash/system/eche/eche_tray_unittest.cc
+++ b/ash/system/eche/eche_tray_unittest.cc
@@ -32,8 +32,7 @@
   void SetUp() override {
     feature_list_.InitWithFeatures(
         /*enabled_features=*/{chromeos::features::kEcheSWA,
-                              chromeos::features::kEcheCustomWidget,
-                              chromeos::features::kEcheSWAInBackground},
+                              chromeos::features::kEcheCustomWidget},
         /*disabled_features=*/{});
 
     DCHECK(test_web_view_factory_.get());
@@ -140,4 +139,38 @@
   EXPECT_FALSE(phone_hub_tray()->eche_loading_indicator()->GetAnimating());
 }
 
+TEST_F(EcheTrayTest, EcheTrayCreatesBubbleButStreamStatusChanged) {
+  // Verify the eche tray button is not active, and the eche tray bubble
+  // is not shown initially.
+  EXPECT_FALSE(eche_tray()->is_active());
+  EXPECT_FALSE(eche_tray()->get_bubble_wrapper_for_test());
+
+  // Allow us to create the bubble but it is not visible until we need this
+  // bubble to show up.
+  eche_tray()->LoadBubble(GURL("http://google.com"), gfx::Image());
+
+  EXPECT_FALSE(eche_tray()->is_active());
+  EXPECT_TRUE(eche_tray()->get_bubble_wrapper_for_test());
+  EXPECT_FALSE(
+      eche_tray()->get_bubble_wrapper_for_test()->bubble_view()->GetVisible());
+
+  // When the streaming status changes, the bubble should show up.
+  eche_tray()->OnStreamStatusChanged(
+      eche_app::mojom::StreamStatus::kStreamStatusStarted);
+  // Wait for the tray bubble widget to open.
+  base::RunLoop().RunUntilIdle();
+  EXPECT_TRUE(eche_tray()->is_active());
+  EXPECT_TRUE(eche_tray()->get_bubble_wrapper_for_test());
+  EXPECT_TRUE(
+      eche_tray()->get_bubble_wrapper_for_test()->bubble_view()->GetVisible());
+
+  // Change the streaming status, the bubble should be closed.
+  eche_tray()->OnStreamStatusChanged(
+      eche_app::mojom::StreamStatus::kStreamStatusStopped);
+  // Wait for the tray bubble widget to close.
+  base::RunLoop().RunUntilIdle();
+  EXPECT_FALSE(eche_tray()->is_active());
+  EXPECT_FALSE(eche_tray()->GetVisible());
+}
+
 }  // namespace ash
diff --git a/ash/system/geolocation/geolocation_controller.cc b/ash/system/geolocation/geolocation_controller.cc
index 3e351d08..b71560a 100644
--- a/ash/system/geolocation/geolocation_controller.cc
+++ b/ash/system/geolocation/geolocation_controller.cc
@@ -39,7 +39,8 @@
 
 GeolocationController::GeolocationController(
     scoped_refptr<network::SharedURLLoaderFactory> factory)
-    : provider_(std::move(factory),
+    : factory_(factory.get()),
+      provider_(std::move(factory),
                 SimpleGeolocationProvider::DefaultGeolocationProviderURL()),
       backoff_delay_(kMinimumDelayAfterFailure),
       timer_(std::make_unique<base::OneShotTimer>()) {
diff --git a/ash/system/geolocation/geolocation_controller.h b/ash/system/geolocation/geolocation_controller.h
index bcb155a3..d5297a89 100644
--- a/ash/system/geolocation/geolocation_controller.h
+++ b/ash/system/geolocation/geolocation_controller.h
@@ -89,6 +89,8 @@
 
   static base::TimeDelta GetNextRequestDelayAfterSuccessForTesting();
 
+  network::SharedURLLoaderFactory* GetFactoryForTesting() { return factory_; }
+
   void SetTimerForTesting(std::unique_ptr<base::OneShotTimer> timer);
 
   void SetClockForTesting(base::Clock* clock);
@@ -128,6 +130,8 @@
   // the chances of getting inaccurate values, especially around DST changes.
   base::Time GetSunRiseSet(bool sunrise) const;
 
+  network::SharedURLLoaderFactory* const factory_;
+
   // The IP-based geolocation provider.
   SimpleGeolocationProvider provider_;
 
diff --git a/ash/system/geolocation/geolocation_controller_test_util.cc b/ash/system/geolocation/geolocation_controller_test_util.cc
new file mode 100644
index 0000000..f9adfff
--- /dev/null
+++ b/ash/system/geolocation/geolocation_controller_test_util.cc
@@ -0,0 +1,38 @@
+// 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 "ash/system/geolocation/geolocation_controller_test_util.h"
+
+namespace ash {
+
+// -----------------------------------------------------------------------------
+// GeolocationControllerObserver:
+
+void GeolocationControllerObserver::OnGeopositionChanged(
+    bool possible_change_in_timezone) {
+  position_received_num_++;
+  possible_change_in_timezone_ = possible_change_in_timezone;
+}
+
+// -----------------------------------------------------------------------------
+// GeopositionResponsesWaiter:
+
+GeopositionResponsesWaiter::GeopositionResponsesWaiter() {
+  GeolocationController::Get()->AddObserver(this);
+}
+
+GeopositionResponsesWaiter::~GeopositionResponsesWaiter() {
+  GeolocationController::Get()->RemoveObserver(this);
+}
+
+void GeopositionResponsesWaiter::Wait() {
+  run_loop_.Run();
+}
+
+void GeopositionResponsesWaiter::OnGeopositionChanged(
+    bool possible_change_in_timezone) {
+  run_loop_.Quit();
+}
+
+}  // namespace ash
\ No newline at end of file
diff --git a/ash/system/geolocation/geolocation_controller_test_util.h b/ash/system/geolocation/geolocation_controller_test_util.h
new file mode 100644
index 0000000..d8ba1c6
--- /dev/null
+++ b/ash/system/geolocation/geolocation_controller_test_util.h
@@ -0,0 +1,62 @@
+// 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 ASH_SYSTEM_GEOLOCATION_GEOLOCATION_CONTROLLER_TEST_UTIL_H_
+#define ASH_SYSTEM_GEOLOCATION_GEOLOCATION_CONTROLLER_TEST_UTIL_H_
+
+#include "ash/system/geolocation/geolocation_controller.h"
+#include "base/run_loop.h"
+
+namespace ash {
+
+// An observer class to GeolocationController which updates sunset and sunrise
+// time.
+class GeolocationControllerObserver : public GeolocationController::Observer {
+ public:
+  GeolocationControllerObserver() = default;
+
+  GeolocationControllerObserver(const GeolocationControllerObserver&) = delete;
+  GeolocationControllerObserver& operator=(
+      const GeolocationControllerObserver&) = delete;
+
+  ~GeolocationControllerObserver() override = default;
+
+  // GeolocationController::Observer:
+  void OnGeopositionChanged(bool possible_change_in_timezone) override;
+
+  int position_received_num() const { return position_received_num_; }
+  bool possible_change_in_timezone() const {
+    return possible_change_in_timezone_;
+  }
+
+ private:
+  // The number of times a new position is received.
+  int position_received_num_ = 0;
+  bool possible_change_in_timezone_ = false;
+};
+
+// Used for waiting for the geoposition to be responded from the server to all
+// observers.
+class GeopositionResponsesWaiter : public GeolocationController::Observer {
+ public:
+  GeopositionResponsesWaiter();
+
+  GeopositionResponsesWaiter(const GeopositionResponsesWaiter&) = delete;
+  GeopositionResponsesWaiter& operator=(const GeopositionResponsesWaiter&) =
+      delete;
+
+  ~GeopositionResponsesWaiter() override;
+
+  void Wait();
+
+  // GeolocationController::Observer:
+  void OnGeopositionChanged(bool possible_change_in_timezone) override;
+
+ private:
+  base::RunLoop run_loop_;
+};
+
+}  // namespace ash
+
+#endif  // ASH_SYSTEM_GEOLOCATION_GEOLOCATION_CONTROLLER_TEST_UTIL_H_
\ No newline at end of file
diff --git a/ash/system/geolocation/geolocation_controller_unittest.cc b/ash/system/geolocation/geolocation_controller_unittest.cc
index 0736e71..c40276d 100644
--- a/ash/system/geolocation/geolocation_controller_unittest.cc
+++ b/ash/system/geolocation/geolocation_controller_unittest.cc
@@ -4,6 +4,9 @@
 
 #include "ash/system/geolocation/geolocation_controller.h"
 
+#include "ash/shell.h"
+#include "ash/system/geolocation/geolocation_controller_test_util.h"
+#include "ash/system/geolocation/test_geolocation_url_loader_factory.h"
 #include "ash/system/time/time_of_day.h"
 #include "ash/test/ash_test_base.h"
 #include "base/strings/utf_string_conversions.h"
@@ -27,82 +30,30 @@
   return chromeos::system::TimezoneSettings::GetTimezoneID(timezone);
 }
 
-// An observer class to GeolocationController which updates sunset and sunrise
-// time.
-class GeolocationControllerObserver : public GeolocationController::Observer {
- public:
-  GeolocationControllerObserver() = default;
-
-  GeolocationControllerObserver(const GeolocationControllerObserver&) = delete;
-  GeolocationControllerObserver& operator=(
-      const GeolocationControllerObserver&) = delete;
-
-  ~GeolocationControllerObserver() override = default;
-
-  // TODO(crbug.com/1269915): Add `sunset_` and `sunrise_` and update their
-  // values when receiving the new position.
-  void OnGeopositionChanged(bool possible_change_in_timezone) override {
-    position_received_num_++;
-  }
-
-  int position_received_num() const { return position_received_num_; }
-
- private:
-  // The number of times a new position is received.
-  int position_received_num_ = 0;
-};
-
-// A fake implementation of GeolocationController that doesn't perform any
-// actual geoposition requests.
-class FakeGeolocationController : public GeolocationController {
- public:
-  FakeGeolocationController(base::SimpleTestClock* test_clock,
-                            std::unique_ptr<base::MockOneShotTimer> mock_timer)
-      : GeolocationController(/*url_context_getter=*/nullptr) {
-    SetTimerForTesting(std::move(mock_timer));
-    SetClockForTesting(test_clock);
-  }
-
-  FakeGeolocationController(const FakeGeolocationController&) = delete;
-  FakeGeolocationController& operator=(const FakeGeolocationController&) =
-      delete;
-
-  ~FakeGeolocationController() override = default;
-
-  void set_position_to_send(const Geoposition& position) {
-    position_to_send_ = position;
-  }
-
-  const Geoposition& position_to_send() const { return position_to_send_; }
-
-  int geoposition_requests_num() const { return geoposition_requests_num_; }
-
- private:
-  // GeolocationController:
-  void RequestGeoposition() override {
-    OnGeoposition(position_to_send_, /*server_error=*/false, base::TimeDelta());
-    ++geoposition_requests_num_;
-  }
-
-  // The position to send to the controller the next time OnGeoposition is
-  // invoked.
-  Geoposition position_to_send_;
-
-  // The number of new geoposition requests that have been triggered.
-  int geoposition_requests_num_ = 0;
-};
-
 // Base test fixture.
 class GeolocationControllerTest : public AshTestBase {
  public:
-  GeolocationControllerTest() {
+  GeolocationControllerTest() = default;
+  GeolocationControllerTest(const GeolocationControllerTest&) = delete;
+  GeolocationControllerTest& operator=(const GeolocationControllerTest&) =
+      delete;
+
+  ~GeolocationControllerTest() override = default;
+
+  // AshTestBase:
+  void SetUp() override {
+    AshTestBase::SetUp();
+    controller_ = ash::Shell::Get()->geolocation_controller();
+
     test_clock_.SetNow(base::Time::Now());
 
     std::unique_ptr<base::MockOneShotTimer> mock_timer =
         std::make_unique<base::MockOneShotTimer>();
     mock_timer_ptr_ = mock_timer.get();
-    controller_ = std::make_unique<FakeGeolocationController>(
-        &test_clock_, std::move(mock_timer));
+    controller_->SetTimerForTesting(std::move(mock_timer));
+    controller_->SetClockForTesting(&test_clock_);
+    factory_ = static_cast<TestGeolocationUrlLoaderFactory*>(
+        controller_->GetFactoryForTesting());
 
     // Prepare a valid geoposition.
     Geoposition position;
@@ -111,120 +62,154 @@
     position.status = Geoposition::STATUS_OK;
     position.accuracy = 10;
     position.timestamp = base::Time::Now();
-    controller_->set_position_to_send(position);
+    SetServerPosition(position);
   }
 
-  GeolocationControllerTest(const GeolocationControllerTest&) = delete;
-  GeolocationControllerTest& operator=(const GeolocationControllerTest&) =
-      delete;
+  GeolocationController* controller() const { return controller_; }
+  base::SimpleTestClock* test_clock() { return &test_clock_; }
+  base::MockOneShotTimer* mock_timer_ptr() const { return mock_timer_ptr_; }
+  const Geoposition& position() const { return position_; }
 
-  ~GeolocationControllerTest() override = default;
+  // Fires the timer of the scheduler to request geoposition and wait for all
+  // observers to receive the latest geoposition from the server.
+  void FireTimerToFetchGeoposition() {
+    GeopositionResponsesWaiter waiter;
+    // Make sure that the timer is running indicating that the client runs
+    // the scheduler.
+    EXPECT_TRUE(mock_timer_ptr()->IsRunning());
+    // Fast forward the scheduler to reach the time when the controller
+    // requests for geoposition from the server in
+    // `GeolocationController::RequestGeoposition`.
+    mock_timer_ptr_->Fire();
+    // Waits for the observers to receive the geoposition from the server.
+    waiter.Wait();
+  }
 
- protected:
-  std::unique_ptr<FakeGeolocationController> controller_;
+  // Sets the geoposition to be returned from the `factory_` upon the
+  // `GeolocationController` request.
+  void SetServerPosition(const Geoposition& position) {
+    position_ = position;
+    factory_->set_position(position_);
+  }
+
+ private:
+  GeolocationController* controller_;
   base::SimpleTestClock test_clock_;
   base::MockOneShotTimer* mock_timer_ptr_;
+  TestGeolocationUrlLoaderFactory* factory_;
+  Geoposition position_;
 };
 
+// Tests adding and removing an observer should request and stop receiving
+// a position update.
+TEST_F(GeolocationControllerTest, Observer) {
+  EXPECT_FALSE(mock_timer_ptr()->IsRunning());
+
+  // Add an observer should start the timer requesting the geoposition.
+  GeolocationControllerObserver observer;
+  controller()->AddObserver(&observer);
+  FireTimerToFetchGeoposition();
+  EXPECT_EQ(1, observer.position_received_num());
+
+  // Check that the timer fires another schedule after a successful request.
+  EXPECT_TRUE(mock_timer_ptr()->IsRunning());
+
+  // Removing an observer should stop the timer.
+  controller()->RemoveObserver(&observer);
+  EXPECT_FALSE(mock_timer_ptr()->IsRunning());
+  EXPECT_EQ(1, observer.position_received_num());
+}
+
 // Tests adding and removing observer and make sure that only observing ones
 // receive the position updates.
 TEST_F(GeolocationControllerTest, MultipleObservers) {
-  EXPECT_EQ(0, controller_->geoposition_requests_num());
-  EXPECT_FALSE(mock_timer_ptr_->IsRunning());
+  EXPECT_FALSE(mock_timer_ptr()->IsRunning());
 
   // Add an observer should start the timer requesting for the first
   // geoposition request.
   GeolocationControllerObserver observer1;
-  controller_->AddObserver(&observer1);
-  EXPECT_TRUE(mock_timer_ptr_->IsRunning());
-  mock_timer_ptr_->Fire();
-  EXPECT_EQ(1, controller_->geoposition_requests_num());
+  controller()->AddObserver(&observer1);
+  FireTimerToFetchGeoposition();
   EXPECT_EQ(1, observer1.position_received_num());
+  EXPECT_TRUE(mock_timer_ptr()->IsRunning());
 
   // Since `OnGeoposition()` handling a geoposition update always schedule
   // the next geoposition request, the timer should keep running and
   // update position periodically.
-  EXPECT_TRUE(mock_timer_ptr_->IsRunning());
-  mock_timer_ptr_->Fire();
-  EXPECT_EQ(2, controller_->geoposition_requests_num());
+  FireTimerToFetchGeoposition();
   EXPECT_EQ(2, observer1.position_received_num());
+  EXPECT_TRUE(mock_timer_ptr()->IsRunning());
 
   // Adding `observer2` should not interrupt the request flow. Check that both
   // observers receive the new position.
   GeolocationControllerObserver observer2;
-  controller_->AddObserver(&observer2);
-  mock_timer_ptr_->Fire();
-  EXPECT_EQ(3, controller_->geoposition_requests_num());
+  controller()->AddObserver(&observer2);
+  FireTimerToFetchGeoposition();
   EXPECT_EQ(3, observer1.position_received_num());
   EXPECT_EQ(1, observer2.position_received_num());
+  EXPECT_TRUE(mock_timer_ptr()->IsRunning());
 
   // Remove `observer1` and make sure that the timer is still running.
   // Only `observer2` should receive the new position.
-  controller_->RemoveObserver(&observer1);
-  mock_timer_ptr_->Fire();
-  EXPECT_EQ(4, controller_->geoposition_requests_num());
+  controller()->RemoveObserver(&observer1);
+  FireTimerToFetchGeoposition();
   EXPECT_EQ(3, observer1.position_received_num());
   EXPECT_EQ(2, observer2.position_received_num());
+  EXPECT_TRUE(mock_timer_ptr()->IsRunning());
 
   // Removing `observer2` should stop the timer. The request count should
   // not change.
-  controller_->RemoveObserver(&observer2);
-  EXPECT_FALSE(mock_timer_ptr_->IsRunning());
-  EXPECT_EQ(4, controller_->geoposition_requests_num());
+  controller()->RemoveObserver(&observer2);
+  EXPECT_FALSE(mock_timer_ptr()->IsRunning());
   EXPECT_EQ(3, observer1.position_received_num());
   EXPECT_EQ(2, observer2.position_received_num());
 }
 
 // Tests that controller only pushes valid positions.
 TEST_F(GeolocationControllerTest, InvalidPositions) {
-  EXPECT_EQ(0, controller_->geoposition_requests_num());
   GeolocationControllerObserver observer;
   // Update to an invalid position
-  Geoposition position = controller_->position_to_send();
-  position.status = Geoposition::STATUS_TIMEOUT;
-  controller_->set_position_to_send(position);
-  EXPECT_FALSE(mock_timer_ptr_->IsRunning());
-  controller_->AddObserver(&observer);
-  EXPECT_TRUE(mock_timer_ptr_->IsRunning());
+  Geoposition invalid_position(position());
+  invalid_position.error_code = 10;
+  SetServerPosition(invalid_position);
+  EXPECT_FALSE(mock_timer_ptr()->IsRunning());
+  controller()->AddObserver(&observer);
 
   // If the position is invalid, the controller won't push the geoposition
   // update to its observers.
-  mock_timer_ptr_->Fire();
-  EXPECT_EQ(1, controller_->geoposition_requests_num());
+  EXPECT_TRUE(mock_timer_ptr()->IsRunning());
+  mock_timer_ptr()->Fire();
+  // Wait for the request and response to finish.
+  base::RunLoop().RunUntilIdle();
   EXPECT_EQ(0, observer.position_received_num());
-
-  // If the position is valid, the controller pushes the update to observers.
-  position.status = Geoposition::STATUS_OK;
-  controller_->set_position_to_send(position);
-  mock_timer_ptr_->Fire();
-  EXPECT_EQ(2, controller_->geoposition_requests_num());
-  EXPECT_EQ(1, observer.position_received_num());
+  // With error response, the server will retry with another timer which we
+  // have no control over, so `mock_timer_ptr_` will not be running (refers to
+  // `SimpleGeolocationRequest::Retry()` for more detail).
+  EXPECT_FALSE(mock_timer_ptr()->IsRunning());
 }
 
 // Tests that timezone changes result.
 TEST_F(GeolocationControllerTest, TimezoneChanges) {
-  EXPECT_EQ(0, controller_->geoposition_requests_num());
-  EXPECT_FALSE(mock_timer_ptr_->IsRunning());
-  controller_->SetCurrentTimezoneIdForTesting(u"America/Los_Angeles");
+  EXPECT_FALSE(mock_timer_ptr()->IsRunning());
+  controller()->SetCurrentTimezoneIdForTesting(u"America/Los_Angeles");
 
   // Add an observer.
   GeolocationControllerObserver observer;
-  controller_->AddObserver(&observer);
+  controller()->AddObserver(&observer);
   EXPECT_EQ(0, observer.position_received_num());
-  EXPECT_TRUE(mock_timer_ptr_->IsRunning());
-  mock_timer_ptr_->Fire();
-  EXPECT_EQ(1, controller_->geoposition_requests_num());
+  FireTimerToFetchGeoposition();
   EXPECT_EQ(1, observer.position_received_num());
-  EXPECT_EQ(u"America/Los_Angeles", controller_->current_timezone_id());
+  EXPECT_EQ(u"America/Los_Angeles", controller()->current_timezone_id());
+  EXPECT_TRUE(mock_timer_ptr()->IsRunning());
 
   // A new timezone results in new geoposition request.
   auto timezone = CreateTimezone("Asia/Tokyo");
 
-  controller_->TimezoneChanged(*timezone);
-  mock_timer_ptr_->Fire();
-  EXPECT_EQ(2, controller_->geoposition_requests_num());
+  controller()->TimezoneChanged(*timezone);
+  FireTimerToFetchGeoposition();
   EXPECT_EQ(2, observer.position_received_num());
-  EXPECT_EQ(GetTimezoneId(*timezone), controller_->current_timezone_id());
+  EXPECT_EQ(GetTimezoneId(*timezone), controller()->current_timezone_id());
+  EXPECT_TRUE(mock_timer_ptr()->IsRunning());
 }
 
 // Tests obtaining sunset/sunrise time when there is no valid geoposition, for
@@ -237,9 +222,9 @@
 
   // If geoposition is unset, the controller should return the default sunset
   // and sunrise time .
-  EXPECT_EQ(controller_->GetSunsetTime(),
+  EXPECT_EQ(controller()->GetSunsetTime(),
             TimeOfDay(kDefaultSunsetTimeOffsetMinutes).ToTimeToday());
-  EXPECT_EQ(controller_->GetSunriseTime(),
+  EXPECT_EQ(controller()->GetSunriseTime(),
             TimeOfDay(kDefaultSunriseTimeOffsetMinutes).ToTimeToday());
 }
 
@@ -248,13 +233,22 @@
 TEST_F(GeolocationControllerTest, GetSunRiseSet) {
   base::Time now;
   EXPECT_TRUE(base::Time::FromUTCString("23 Dec 2021 12:00:00", &now));
-  test_clock_.SetNow(now);
+  test_clock()->SetNow(now);
 
   base::Time sunrise;
   EXPECT_TRUE(base::Time::FromUTCString("23 Dec 2021 04:14:36.626", &sunrise));
   base::Time sunset;
   EXPECT_TRUE(base::Time::FromUTCString("23 Dec 2021 14:59:58.459", &sunset));
 
+  // Add an observer and make sure that sunset and sunrise time are not
+  // updated until the timer is fired.
+  GeolocationControllerObserver observer1;
+  controller()->AddObserver(&observer1);
+  EXPECT_TRUE(mock_timer_ptr()->IsRunning());
+  EXPECT_NE(controller()->GetSunsetTime(), sunset);
+  EXPECT_NE(controller()->GetSunriseTime(), sunrise);
+  EXPECT_EQ(0, observer1.position_received_num());
+
   // Prepare a valid geoposition.
   Geoposition position;
   position.latitude = 23.5;
@@ -263,16 +257,14 @@
   position.accuracy = 10;
   position.timestamp = now;
 
-  GeolocationControllerObserver observer1;
-  controller_->AddObserver(&observer1);
-  EXPECT_TRUE(mock_timer_ptr_->IsRunning());
-  controller_->set_position_to_send(position);
-
-  EXPECT_EQ(0, controller_->geoposition_requests_num());
-  mock_timer_ptr_->Fire();
-  EXPECT_EQ(1, controller_->geoposition_requests_num());
-  EXPECT_EQ(controller_->GetSunsetTime(), sunset);
-  EXPECT_EQ(controller_->GetSunriseTime(), sunrise);
+  // Test that after sending the new position, sunrise and sunset time are
+  // updated correctly.
+  SetServerPosition(position);
+  FireTimerToFetchGeoposition();
+  EXPECT_EQ(1, observer1.position_received_num());
+  EXPECT_EQ(controller()->GetSunsetTime(), sunset);
+  EXPECT_EQ(controller()->GetSunriseTime(), sunrise);
+  EXPECT_TRUE(mock_timer_ptr()->IsRunning());
 }
 
 }  // namespace
diff --git a/ash/system/geolocation/test_geolocation_url_loader_factory.cc b/ash/system/geolocation/test_geolocation_url_loader_factory.cc
index 18b3c9d..155204f 100644
--- a/ash/system/geolocation/test_geolocation_url_loader_factory.cc
+++ b/ash/system/geolocation/test_geolocation_url_loader_factory.cc
@@ -66,6 +66,10 @@
   return nullptr;
 }
 
+void TestGeolocationUrlLoaderFactory::ClearResponses() {
+  test_url_loader_factory_.ClearResponses();
+}
+
 TestGeolocationUrlLoaderFactory::~TestGeolocationUrlLoaderFactory() = default;
 
 }  // namespace ash
diff --git a/ash/system/geolocation/test_geolocation_url_loader_factory.h b/ash/system/geolocation/test_geolocation_url_loader_factory.h
index 0653ee5..61416d1 100644
--- a/ash/system/geolocation/test_geolocation_url_loader_factory.h
+++ b/ash/system/geolocation/test_geolocation_url_loader_factory.h
@@ -39,6 +39,9 @@
   void set_position(Geoposition position) { position_ = position; }
   const Geoposition& position() const { return position_; }
 
+  // Clears all added responses in `test_url_loader_factory_`.
+  void ClearResponses();
+
  protected:
   ~TestGeolocationUrlLoaderFactory() override;
 
diff --git a/ash/system/network/fake_network_detailed_network_view.cc b/ash/system/network/fake_network_detailed_network_view.cc
new file mode 100644
index 0000000..0d08f96
--- /dev/null
+++ b/ash/system/network/fake_network_detailed_network_view.cc
@@ -0,0 +1,23 @@
+// 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 "ash/system/network/fake_network_detailed_network_view.h"
+
+#include "ash/system/network/network_detailed_network_view.h"
+
+namespace ash {
+namespace tray {
+
+FakeNetworkDetailedNetworkView::FakeNetworkDetailedNetworkView(
+    Delegate* delegate)
+    : NetworkDetailedNetworkView(delegate) {}
+
+FakeNetworkDetailedNetworkView::~FakeNetworkDetailedNetworkView() = default;
+
+views::View* FakeNetworkDetailedNetworkView::GetAsView() {
+  return this;
+}
+
+}  // namespace tray
+}  // namespace ash
\ No newline at end of file
diff --git a/ash/system/network/fake_network_detailed_network_view.h b/ash/system/network/fake_network_detailed_network_view.h
new file mode 100644
index 0000000..682eab4
--- /dev/null
+++ b/ash/system/network/fake_network_detailed_network_view.h
@@ -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.
+
+#ifndef ASH_SYSTEM_NETWORK_FAKE_NETWORK_DETAILED_NETWORK_VIEW_H_
+#define ASH_SYSTEM_NETWORK_FAKE_NETWORK_DETAILED_NETWORK_VIEW_H_
+
+#include "ash/ash_export.h"
+#include "ash/system/network/network_detailed_network_view.h"
+#include "ui/views/view.h"
+
+namespace ash {
+namespace tray {
+
+// Fake implementation of NetworkDetailedNetworkView.
+class ASH_EXPORT FakeNetworkDetailedNetworkView
+    : public NetworkDetailedNetworkView,
+      public views::View {
+ public:
+  explicit FakeNetworkDetailedNetworkView(Delegate* delegate);
+  FakeNetworkDetailedNetworkView(const FakeNetworkDetailedNetworkView&) =
+      delete;
+  FakeNetworkDetailedNetworkView& operator=(
+      const FakeNetworkDetailedNetworkView&) = delete;
+  ~FakeNetworkDetailedNetworkView() override;
+
+ private:
+  // NetworkDetailedNetworkView:
+  views::View* GetAsView() override;
+};
+
+}  // namespace tray
+}  // namespace ash
+
+#endif  // ASH_SYSTEM_NETWORK_FAKE_NETWORK_DETAILED_NETWORK_VIEW_H_
diff --git a/ash/system/network/network_list_view_controller.cc b/ash/system/network/network_list_view_controller.cc
new file mode 100644
index 0000000..40b7d0bd
--- /dev/null
+++ b/ash/system/network/network_list_view_controller.cc
@@ -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.
+
+#include "ash/system/network/network_list_view_controller.h"
+
+#include "ash/system/network/network_detailed_network_view.h"
+#include "ash/system/network/network_list_view_controller_impl.h"
+
+namespace ash {
+
+namespace tray {
+
+namespace {
+NetworkListViewController::Factory* g_test_factory = nullptr;
+}  // namespace
+
+std::unique_ptr<NetworkListViewController>
+NetworkListViewController::Factory::Create(
+    tray::NetworkDetailedNetworkView* network_detailed_network_view) {
+  if (g_test_factory)
+    return g_test_factory->CreateForTesting();  // IN-TEST
+  return std::make_unique<NetworkListViewControllerImpl>(
+      network_detailed_network_view);
+}
+
+void NetworkListViewController::Factory::SetFactoryForTesting(
+    Factory* test_factory) {
+  g_test_factory = test_factory;
+}
+
+}  // namespace tray
+}  // namespace ash
\ No newline at end of file
diff --git a/ash/system/network/network_list_view_controller.h b/ash/system/network/network_list_view_controller.h
new file mode 100644
index 0000000..487d391
--- /dev/null
+++ b/ash/system/network/network_list_view_controller.h
@@ -0,0 +1,48 @@
+// 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 ASH_SYSTEM_NETWORK_NETWORK_LIST_VIEW_CONTROLLER_H_
+#define ASH_SYSTEM_NETWORK_NETWORK_LIST_VIEW_CONTROLLER_H_
+
+#include "ash/ash_export.h"
+#include "ash/system/network/network_detailed_network_view.h"
+
+namespace ash {
+namespace tray {
+
+// This class defines the interface used to add, modify, and remove networks
+// from the network list of the detailed network device page within the quick
+// settings. This class includes the definition of the factory used to create
+// instances of implementations of this class.
+class ASH_EXPORT NetworkListViewController {
+ public:
+  class Factory {
+   public:
+    Factory(const Factory&) = delete;
+    const Factory& operator=(const Factory&) = delete;
+    virtual ~Factory() = default;
+
+    static std::unique_ptr<NetworkListViewController> Create(
+        tray::NetworkDetailedNetworkView* network_detailed_network_view);
+    static void SetFactoryForTesting(Factory* test_factory);
+
+   protected:
+    Factory() = default;
+
+    virtual std::unique_ptr<NetworkListViewController> CreateForTesting() = 0;
+  };
+
+  NetworkListViewController(const NetworkListViewController&) = delete;
+  NetworkListViewController& operator=(const NetworkListViewController&) =
+      delete;
+  virtual ~NetworkListViewController() = default;
+
+ protected:
+  NetworkListViewController() = default;
+};
+
+}  // namespace tray
+}  // namespace ash
+
+#endif  // ASH_SYSTEM_NETWORK_NETWORK_LIST_VIEW_CONTROLLER_H_
diff --git a/ash/system/network/network_list_view_controller_impl.cc b/ash/system/network/network_list_view_controller_impl.cc
new file mode 100644
index 0000000..d76f9999
--- /dev/null
+++ b/ash/system/network/network_list_view_controller_impl.cc
@@ -0,0 +1,42 @@
+// 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 "ash/system/network/network_list_view_controller_impl.h"
+
+#include "ash/constants/ash_features.h"
+#include "ash/shell.h"
+#include "ash/system/model/system_tray_model.h"
+#include "ash/system/network/network_detailed_network_view.h"
+#include "ash/system/network/tray_network_state_model.h"
+
+namespace ash {
+namespace tray {
+
+NetworkListViewControllerImpl::NetworkListViewControllerImpl(
+    NetworkDetailedNetworkView* network_detailed_network_view)
+    : network_detailed_network_view_(network_detailed_network_view) {
+  DCHECK(ash::features::IsQuickSettingsNetworkRevampEnabled());
+  DCHECK(network_detailed_network_view_);
+  Shell::Get()->system_tray_model()->network_state_model()->AddObserver(this);
+}
+
+NetworkListViewControllerImpl::~NetworkListViewControllerImpl() {
+  Shell::Get()->system_tray_model()->network_state_model()->RemoveObserver(
+      this);
+}
+
+void NetworkListViewControllerImpl::ActiveNetworkStateChanged() {
+  // TODO(b/207089013): Implement this function.
+}
+
+void NetworkListViewControllerImpl::NetworkListChanged() {
+  // TODO(b/207089013): Implement this function.
+}
+
+void NetworkListViewControllerImpl::DeviceStateListChanged() {
+  // TODO(b/207089013): Implement this function.
+}
+
+}  // namespace tray
+}  // namespace ash
\ No newline at end of file
diff --git a/ash/system/network/network_list_view_controller_impl.h b/ash/system/network/network_list_view_controller_impl.h
new file mode 100644
index 0000000..658f3339
--- /dev/null
+++ b/ash/system/network/network_list_view_controller_impl.h
@@ -0,0 +1,47 @@
+// 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 ASH_SYSTEM_NETWORK_NETWORK_LIST_VIEW_CONTROLLER_IMPL_H_
+#define ASH_SYSTEM_NETWORK_NETWORK_LIST_VIEW_CONTROLLER_IMPL_H_
+
+#include "ash/ash_export.h"
+
+#include "ash/system/network/network_list_view_controller.h"
+#include "ash/system/network/tray_network_state_observer.h"
+
+namespace ash {
+namespace tray {
+
+class NetworkDetailedNetworkView;
+
+// Implementation of NetworkListViewController
+class ASH_EXPORT NetworkListViewControllerImpl
+    : public TrayNetworkStateObserver,
+      public NetworkListViewController {
+ public:
+  NetworkListViewControllerImpl(
+      NetworkDetailedNetworkView* network_detailed_network_view);
+  NetworkListViewControllerImpl(const NetworkListViewController&) = delete;
+  NetworkListViewControllerImpl& operator=(
+      const NetworkListViewControllerImpl&) = delete;
+  ~NetworkListViewControllerImpl() override;
+
+ protected:
+  tray::NetworkDetailedNetworkView* network_detailed_network_view() {
+    return network_detailed_network_view_;
+  }
+
+ private:
+  // TrayNetworkStateObserver:
+  void ActiveNetworkStateChanged() override;
+  void NetworkListChanged() override;
+  void DeviceStateListChanged() override;
+
+  tray::NetworkDetailedNetworkView* network_detailed_network_view_;
+};
+
+}  // namespace tray
+}  // namespace ash
+
+#endif  // ASH_SYSTEM_NETWORK_NETWORK_LIST_VIEW_CONTROLLER_IMPL_H_
diff --git a/ash/system/network/network_list_view_controller_unittest.cc b/ash/system/network/network_list_view_controller_unittest.cc
new file mode 100644
index 0000000..959a57b
--- /dev/null
+++ b/ash/system/network/network_list_view_controller_unittest.cc
@@ -0,0 +1,61 @@
+// 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 "ash/system/network/network_list_view_controller_impl.h"
+
+#include <memory>
+
+#include "ash/constants/ash_features.h"
+#include "ash/system/network/fake_network_detailed_network_view.h"
+#include "ash/test/ash_test_base.h"
+#include "base/test/scoped_feature_list.h"
+
+namespace ash {
+namespace tray {
+
+class NetworkListViewControllerTest : public AshTestBase {
+ public:
+  void SetUp() override {
+    AshTestBase::SetUp();
+
+    feature_list_.InitAndEnableFeature(features::kQuickSettingsNetworkRevamp);
+
+    fake_network_detailed_network_view_ =
+        std::make_unique<FakeNetworkDetailedNetworkView>(
+            /*delegate=*/nullptr);
+
+    network_list_view_controller_impl_ =
+        std::make_unique<NetworkListViewControllerImpl>(
+            fake_network_detailed_network_view_.get());
+  }
+
+  void TearDown() override {
+    network_list_view_controller_impl_.reset();
+    fake_network_detailed_network_view_.reset();
+
+    AshTestBase::TearDown();
+  }
+
+  FakeNetworkDetailedNetworkView* fake_network_detailed_network_view() {
+    return fake_network_detailed_network_view_.get();
+  }
+
+  NetworkListViewController* network_list_view_controller_impl() {
+    return network_list_view_controller_impl_.get();
+  }
+
+ private:
+  base::test::ScopedFeatureList feature_list_;
+  std::unique_ptr<FakeNetworkDetailedNetworkView>
+      fake_network_detailed_network_view_;
+  std::unique_ptr<NetworkListViewControllerImpl>
+      network_list_view_controller_impl_;
+};
+
+TEST_F(NetworkListViewControllerTest, CanConstruct) {
+  EXPECT_TRUE(true);
+}
+
+}  // namespace tray
+}  // namespace ash
\ No newline at end of file
diff --git a/ash/system/scheduled_feature/scheduled_feature_unittest.cc b/ash/system/scheduled_feature/scheduled_feature_unittest.cc
index 5934338..5b4027c 100644
--- a/ash/system/scheduled_feature/scheduled_feature_unittest.cc
+++ b/ash/system/scheduled_feature/scheduled_feature_unittest.cc
@@ -15,12 +15,13 @@
 #include "ash/session/test_session_controller_client.h"
 #include "ash/shell.h"
 #include "ash/system/geolocation/geolocation_controller.h"
+#include "ash/system/geolocation/geolocation_controller_test_util.h"
+#include "ash/system/geolocation/test_geolocation_url_loader_factory.h"
 #include "ash/system/scheduled_feature/scheduled_feature.h"
 #include "ash/system/time/time_of_day.h"
 #include "ash/test/ash_test_base.h"
 #include "ash/test/ash_test_helper.h"
 #include "ash/test_shell_delegate.h"
-#include "base/bind.h"
 #include "base/command_line.h"
 #include "base/memory/ptr_util.h"
 #include "base/strings/pattern.h"
@@ -70,69 +71,6 @@
   const char* GetFeatureName() const override { return "TestFeature"; }
 };
 
-// An observer class to GeolocationController which updates sunset and sunrise
-// time.
-class GeolocationControllerObserver : public GeolocationController::Observer {
- public:
-  GeolocationControllerObserver() = default;
-
-  GeolocationControllerObserver(const GeolocationControllerObserver&) = delete;
-  GeolocationControllerObserver& operator=(
-      const GeolocationControllerObserver&) = delete;
-
-  ~GeolocationControllerObserver() override = default;
-
-  // TODO(crbug.com/1269915): Add `sunset_` and `sunrise_` and update their
-  // values when receiving the new position.
-  void OnGeopositionChanged(bool possible_change_in_timezone) override {
-    position_received_num_++;
-    possible_change_in_timezone_ = possible_change_in_timezone;
-  }
-
-  int position_received_num() const { return position_received_num_; }
-  bool possible_change_in_timezone() const {
-    return possible_change_in_timezone_;
-  }
-
- private:
-  // The number of times a new position is received.
-  int position_received_num_ = 0;
-  bool possible_change_in_timezone_ = false;
-};
-
-// A fake implementation of GeolocationController that doesn't perform any
-// actual geoposition requests.
-class FakeGeolocationController : public GeolocationController {
- public:
-  FakeGeolocationController(base::SimpleTestClock* test_clock,
-                            std::unique_ptr<base::MockOneShotTimer> mock_timer)
-      : GeolocationController(/*url_context_getter=*/nullptr) {
-    SetTimerForTesting(std::move(mock_timer));
-    SetClockForTesting(test_clock);
-  }
-
-  FakeGeolocationController(const FakeGeolocationController&) = delete;
-  FakeGeolocationController& operator=(const FakeGeolocationController&) =
-      delete;
-
-  ~FakeGeolocationController() override = default;
-
-  void set_position_to_send(const Geoposition& position) {
-    position_to_send_ = position;
-  }
-
- protected:
-  // GeolocationController:
-  void RequestGeoposition() override {
-    OnGeoposition(position_to_send_, /*server_error=*/false, base::TimeDelta());
-  }
-
- private:
-  // The position to send to the observer by the controller the next time
-  // `OnGeoposition()` is invoked.
-  Geoposition position_to_send_;
-};
-
 class ScheduledFeatureTest : public NoSessionAshTestBase {
  public:
   ScheduledFeatureTest() = default;
@@ -150,7 +88,15 @@
         AccountId::FromUserEmail(kUser2Email));
   }
 
-  ScheduledFeature* feature() { return feature_.get(); }
+  TestScheduledFeature* feature() const { return feature_.get(); }
+  GeolocationController* geolocation_controller() {
+    return geolocation_controller_;
+  }
+  base::SimpleTestClock* test_clock() { return &test_clock_; }
+  const base::MockOneShotTimer* mock_timer_ptr() const {
+    return mock_timer_ptr_;
+  }
+  TestGeolocationUrlLoaderFactory* factory() const { return factory_; }
 
   // AshTestBase:
   void SetUp() override {
@@ -161,12 +107,6 @@
     // Simulate user 1 login.
     SimulateNewUserFirstLogin(kUser1Email);
 
-    std::unique_ptr<base::MockOneShotTimer> mock_timer =
-        std::make_unique<base::MockOneShotTimer>();
-    mock_timer_ptr_ = mock_timer.get();
-    geolocation_controller_ = std::make_unique<FakeGeolocationController>(
-        &test_clock_, std::move(mock_timer));
-
     // Use user prefs of NightLight, which is an example of ScheduledFeature.
     feature_ = std::make_unique<TestScheduledFeature>(
         prefs::kNightLightEnabled, prefs::kNightLightScheduleType,
@@ -177,13 +117,28 @@
     feature_->OnActiveUserPrefServiceChanged(
         Shell::Get()->session_controller()->GetActivePrefService());
 
+    // Set the clock of geolocation controller to our test clock to control the
+    // time now.
+    geolocation_controller_ = ash::Shell::Get()->geolocation_controller();
     base::Time now;
     EXPECT_TRUE(base::Time::FromUTCString("23 Dec 2021 12:00:00", &now));
-    test_clock_.SetNow(now);
+    test_clock()->SetNow(now);
+    geolocation_controller()->SetClockForTesting(&test_clock_);
+
+    // Set the timer of geolocation controller to `mock_timer` to allow us to
+    // trigger new geoposition receiving.
+    std::unique_ptr<base::MockOneShotTimer> mock_timer =
+        std::make_unique<base::MockOneShotTimer>();
+    mock_timer_ptr_ = mock_timer.get();
+    geolocation_controller()->SetTimerForTesting(std::move(mock_timer));
+
+    // `factory_` allows the test to control the value of geoposition
+    // that the geolocation provider sends back upon geolocation request.
+    factory_ = static_cast<TestGeolocationUrlLoaderFactory*>(
+        geolocation_controller()->GetFactoryForTesting());
   }
 
   void TearDown() override {
-    geolocation_controller_.reset();
     feature_.reset();
     NoSessionAshTestBase::TearDown();
   }
@@ -227,11 +182,35 @@
     return TimeOfDay(hour * 60).SetClock(&test_clock_);
   }
 
- protected:
+  // Fires the timer of the scheduler to request geoposition and wait for all
+  // observers to receive the latest geoposition from the server.
+  void FireTimerToFetchGeoposition() {
+    GeopositionResponsesWaiter waiter;
+    // Make sure that the timer is running indicating that the client runs
+    // the scheduler.
+    EXPECT_TRUE(mock_timer_ptr()->IsRunning());
+    // Fast forward the scheduler to reach the time when the controller
+    // requests for geoposition from the server in
+    // `GeolocationController::RequestGeoposition`.
+    mock_timer_ptr_->Fire();
+    // Waits for the observers to receive the geoposition from the server.
+    waiter.Wait();
+  }
+
+  // Sets the geoposition to be returned from the `factory_` upon the
+  // `GeolocationController` request.
+  void SetServerPosition(const Geoposition& position) {
+    position_ = position;
+    factory_->set_position(position_);
+  }
+
+ private:
   std::unique_ptr<TestScheduledFeature> feature_;
-  std::unique_ptr<FakeGeolocationController> geolocation_controller_;
+  GeolocationController* geolocation_controller_;
   base::SimpleTestClock test_clock_;
   base::MockOneShotTimer* mock_timer_ptr_;
+  TestGeolocationUrlLoaderFactory* factory_;
+  Geoposition position_;
 };
 
 // Tests that switching users retrieves the feature settings for the active
@@ -267,7 +246,7 @@
 // types.
 TEST_F(ScheduledFeatureTest, ScheduleNoneToCustomTransition) {
   // Now is 6:00 PM.
-  test_clock_.SetNow(MakeTimeOfDay(6, AmPm::kPM).ToTimeToday());
+  test_clock()->SetNow(MakeTimeOfDay(6, AmPm::kPM).ToTimeToday());
   SetFeatureEnabled(false);
   feature()->SetScheduleType(ScheduledFeature::ScheduleType::kNone);
   // Start time is at 3:00 PM and end time is at 8:00 PM.
@@ -301,7 +280,7 @@
 // Tests what happens when the time now reaches the end of the feature
 // interval when the feature mode is on.
 TEST_F(ScheduledFeatureTest, TestCustomScheduleReachingEndTime) {
-  test_clock_.SetNow(MakeTimeOfDay(6, AmPm::kPM).ToTimeToday());
+  test_clock()->SetNow(MakeTimeOfDay(6, AmPm::kPM).ToTimeToday());
   feature()->SetCustomStartTime(MakeTimeOfDay(3, AmPm::kPM));
   feature()->SetCustomEndTime(MakeTimeOfDay(8, AmPm::kPM));
   feature()->SetScheduleType(ScheduledFeature::ScheduleType::kCustom);
@@ -316,7 +295,7 @@
   //      start                    end & now
   //
   // Now is 8:00 PM.
-  test_clock_.SetNow(MakeTimeOfDay(8, AmPm::kPM).ToTimeToday());
+  test_clock()->SetNow(MakeTimeOfDay(8, AmPm::kPM).ToTimeToday());
   feature()->timer()->FireNow();
   EXPECT_FALSE(GetEnabled());
   // The timer should still be running, but now scheduling the start at 3:00 PM
@@ -335,7 +314,7 @@
   //        |                   |              |
   //      start                end            now
   //
-  test_clock_.SetNow(MakeTimeOfDay(11, AmPm::kPM).ToTimeToday());
+  test_clock()->SetNow(MakeTimeOfDay(11, AmPm::kPM).ToTimeToday());
   feature()->SetCustomStartTime(MakeTimeOfDay(3, AmPm::kPM));
   feature()->SetCustomEndTime(MakeTimeOfDay(8, AmPm::kPM));
   feature()->SetScheduleType(ScheduledFeature::ScheduleType::kCustom);
@@ -370,7 +349,7 @@
   //        |             |             |
   //       now          start          end
   //
-  test_clock_.SetNow(MakeTimeOfDay(4, AmPm::kPM).ToTimeToday());  // 4:00 PM.
+  test_clock()->SetNow(MakeTimeOfDay(4, AmPm::kPM).ToTimeToday());  // 4:00 PM.
   SetFeatureEnabled(false);
   feature()->SetScheduleType(ScheduledFeature::ScheduleType::kNone);
   feature()->SetCustomStartTime(MakeTimeOfDay(6, AmPm::kPM));  // 6:00 PM.
@@ -406,33 +385,33 @@
 
   // Set time now to 10:00 AM.
   base::Time current_time = MakeTimeOfDay(10, AmPm::kAM).ToTimeToday();
-  test_clock_.SetNow(current_time);
+  test_clock()->SetNow(current_time);
   EXPECT_FALSE(feature()->timer()->IsRunning());
   feature()->SetScheduleType(ScheduledFeature::ScheduleType::kSunsetToSunrise);
   EXPECT_FALSE(GetEnabled());
   EXPECT_TRUE(feature()->timer()->IsRunning());
-  EXPECT_EQ(geolocation_controller_->GetSunsetTime() - current_time,
+  EXPECT_EQ(geolocation_controller()->GetSunsetTime() - current_time,
             feature()->timer()->GetCurrentDelay());
 
   // Firing a timer should to advance the time to sunset and automatically turn
   // on the feature.
-  current_time = geolocation_controller_->GetSunsetTime();
-  test_clock_.SetNow(current_time);
+  current_time = geolocation_controller()->GetSunsetTime();
+  test_clock()->SetNow(current_time);
   feature()->timer()->FireNow();
   EXPECT_TRUE(feature()->timer()->IsRunning());
   EXPECT_TRUE(GetEnabled());
   EXPECT_EQ(
-      geolocation_controller_->GetSunriseTime() + base::Days(1) - current_time,
+      geolocation_controller()->GetSunriseTime() + base::Days(1) - current_time,
       feature()->timer()->GetCurrentDelay());
 
   // Firing a timer should advance the time to sunrise and automatically turn
   // off the feature.
-  current_time = geolocation_controller_->GetSunriseTime();
-  test_clock_.SetNow(current_time);
+  current_time = geolocation_controller()->GetSunriseTime();
+  test_clock()->SetNow(current_time);
   feature()->timer()->FireNow();
   EXPECT_FALSE(GetEnabled());
   EXPECT_TRUE(feature()->timer()->IsRunning());
-  EXPECT_EQ(geolocation_controller_->GetSunsetTime() - current_time,
+  EXPECT_EQ(geolocation_controller()->GetSunsetTime() - current_time,
             feature()->timer()->GetCurrentDelay());
 }
 
@@ -451,28 +430,29 @@
   //       now        sunset            sunrise
   //
 
-  // Prepare a valid geoposition.
-  const Geoposition position = CreateGeoposition(
-      kFakePosition1_Latitude, kFakePosition1_Longitude, test_clock_.Now());
-
   GeolocationControllerObserver observer1;
-  geolocation_controller_->AddObserver(&observer1);
-  EXPECT_TRUE(mock_timer_ptr_->IsRunning());
-  geolocation_controller_->set_position_to_send(position);
+  geolocation_controller()->AddObserver(&observer1);
+  EXPECT_TRUE(mock_timer_ptr()->IsRunning());
   EXPECT_FALSE(observer1.possible_change_in_timezone());
 
-  // Fire timer to fetch position update.
-  mock_timer_ptr_->Fire();
+  // Prepare a valid geoposition.
+  const Geoposition position = CreateGeoposition(
+      kFakePosition1_Latitude, kFakePosition1_Longitude, test_clock()->Now());
+
+  // Set and fetch position update.
+  SetServerPosition(position);
+  FireTimerToFetchGeoposition();
   EXPECT_TRUE(observer1.possible_change_in_timezone());
-  const base::Time sunset_time1 = geolocation_controller_->GetSunsetTime();
-  const base::Time sunrise_time1 = geolocation_controller_->GetSunriseTime();
+  EXPECT_TRUE(mock_timer_ptr()->IsRunning());
+  const base::Time sunset_time1 = geolocation_controller()->GetSunsetTime();
+  const base::Time sunrise_time1 = geolocation_controller()->GetSunriseTime();
   // Our assumption is that GeolocationController gives us sunrise time
   // earlier in the same day before sunset.
   ASSERT_GT(sunset_time1, sunrise_time1);
   ASSERT_LT(sunset_time1 - base::Days(1), sunrise_time1);
 
   // Set time now to be 4 hours before sunset.
-  test_clock_.SetNow(sunset_time1 - base::Hours(4));
+  test_clock()->SetNow(sunset_time1 - base::Hours(4));
 
   // Expect that timer is running and the start is scheduled after 4 hours.
   EXPECT_FALSE(feature()->GetEnabled());
@@ -482,16 +462,16 @@
   EXPECT_EQ(base::Hours(4), feature()->timer()->GetCurrentDelay());
 
   // Simulate reaching sunset.
-  test_clock_.SetNow(sunset_time1);  // Now is sunset time of the position1.
+  test_clock()->SetNow(sunset_time1);  // Now is sunset time of the position1.
   feature()->timer()->FireNow();
   EXPECT_TRUE(feature()->GetEnabled());
   // Timer is running scheduling the end at sunrise of the second day.
   EXPECT_TRUE(feature()->timer()->IsRunning());
-  EXPECT_EQ(sunrise_time1 + base::Days(1) - test_clock_.Now(),
+  EXPECT_EQ(sunrise_time1 + base::Days(1) - test_clock()->Now(),
             feature()->timer()->GetCurrentDelay());
 
   // Simulate reaching sunrise.
-  test_clock_.SetNow(sunrise_time1);  // Now is sunrise time of the position1
+  test_clock()->SetNow(sunrise_time1);  // Now is sunrise time of the position1
 
   // Now simulate user changing position.
   // Position 2 sunset and sunrise times.
@@ -502,35 +482,39 @@
   //
 
   const Geoposition position2 = CreateGeoposition(
-      kFakePosition2_Latitude, kFakePosition2_Longitude, test_clock_.Now());
-  geolocation_controller_->set_position_to_send(position2);
-  mock_timer_ptr_->Fire();
+      kFakePosition2_Latitude, kFakePosition2_Longitude, test_clock()->Now());
+  // Replace a response `position` with `position2`.
+  factory()->ClearResponses();
+  SetServerPosition(position2);
+  FireTimerToFetchGeoposition();
   EXPECT_TRUE(observer1.possible_change_in_timezone());
-  const base::Time sunset_time2 = geolocation_controller_->GetSunsetTime();
-  const base::Time sunrise_time2 = geolocation_controller_->GetSunriseTime();
+  EXPECT_TRUE(mock_timer_ptr()->IsRunning());
+
+  const base::Time sunset_time2 = geolocation_controller()->GetSunsetTime();
+  const base::Time sunrise_time2 = geolocation_controller()->GetSunriseTime();
   // We choose the second location such that the new sunrise time is later
   // in the day compared to the old sunrise time, which is also the current
   // time.
-  ASSERT_GT(test_clock_.Now(), sunset_time2);
-  ASSERT_LT(test_clock_.Now(), sunset_time2 + base::Days(1));
-  ASSERT_GT(test_clock_.Now(), sunrise_time2);
-  ASSERT_LT(test_clock_.Now(), sunrise_time2 + base::Days(1));
+  ASSERT_GT(test_clock()->Now(), sunset_time2);
+  ASSERT_LT(test_clock()->Now(), sunset_time2 + base::Days(1));
+  ASSERT_GT(test_clock()->Now(), sunrise_time2);
+  ASSERT_LT(test_clock()->Now(), sunrise_time2 + base::Days(1));
 
   // Expect that the scheduled end delay has been updated to sunrise of the
   // same (second) day in location 2, and the status hasn't changed.
   EXPECT_TRUE(feature()->GetEnabled());
   EXPECT_TRUE(feature()->timer()->IsRunning());
-  EXPECT_EQ(sunrise_time2 + base::Days(1) - test_clock_.Now(),
+  EXPECT_EQ(sunrise_time2 + base::Days(1) - test_clock()->Now(),
             feature()->timer()->GetCurrentDelay());
 
   // Simulate reaching sunrise.
-  test_clock_.SetNow(sunrise_time2 +
-                     base::Days(1));  // Now is sunrise time of the position2.
+  test_clock()->SetNow(sunrise_time2 +
+                       base::Days(1));  // Now is sunrise time of the position2.
   feature()->timer()->FireNow();
   EXPECT_FALSE(feature()->GetEnabled());
   // Timer is running scheduling the start at the sunset of the next day.
   EXPECT_TRUE(feature()->timer()->IsRunning());
-  EXPECT_EQ(sunset_time2 + base::Days(1) - test_clock_.Now(),
+  EXPECT_EQ(sunset_time2 + base::Days(1) - test_clock()->Now(),
             feature()->timer()->GetCurrentDelay());
 }
 
@@ -538,7 +522,7 @@
 // correctly if the time has changed meanwhile.
 TEST_F(ScheduledFeatureTest, CustomScheduleOnResume) {
   // Now is 4:00 PM.
-  test_clock_.SetNow(MakeTimeOfDay(4, AmPm::kPM).ToTimeToday());
+  test_clock()->SetNow(MakeTimeOfDay(4, AmPm::kPM).ToTimeToday());
   feature()->SetEnabled(false);
   // Start time is at 6:00 PM and end time is at 10:00 PM. The feature should be
   // off.
@@ -558,7 +542,7 @@
 
   // Now simulate that the device was suspended for 3 hours, and the time now
   // is 7:00 PM when the devices was resumed. Expect that the feature turns on.
-  test_clock_.SetNow(MakeTimeOfDay(7, AmPm::kPM).ToTimeToday());
+  test_clock()->SetNow(MakeTimeOfDay(7, AmPm::kPM).ToTimeToday());
   feature()->SuspendDone(base::TimeDelta::Max());
 
   EXPECT_TRUE(feature()->GetEnabled());
@@ -575,7 +559,7 @@
 // Case 1: "Now" is less than both "end" and "start".
 TEST_F(ScheduledFeatureTest, CustomScheduleInvertedStartAndEndTimesCase1) {
   // Now is 4:00 AM.
-  test_clock_.SetNow(MakeTimeOfDay(4, AmPm::kAM).ToTimeToday());
+  test_clock()->SetNow(MakeTimeOfDay(4, AmPm::kAM).ToTimeToday());
   SetFeatureEnabled(false);
   // Start time is at 9:00 PM and end time is at 6:00 AM. "Now" is less than
   // both. The feature should be on.
@@ -597,7 +581,7 @@
 // Case 2: "Now" is between "end" and "start".
 TEST_F(ScheduledFeatureTest, CustomScheduleInvertedStartAndEndTimesCase2) {
   // Now is 6:00 AM.
-  test_clock_.SetNow(MakeTimeOfDay(6, AmPm::kAM).ToTimeToday());
+  test_clock()->SetNow(MakeTimeOfDay(6, AmPm::kAM).ToTimeToday());
   SetFeatureEnabled(false);
   // Start time is at 9:00 PM and end time is at 4:00 AM. "Now" is between both.
   // The feature should be off.
@@ -619,7 +603,7 @@
 // Case 3: "Now" is greater than both "start" and "end".
 TEST_F(ScheduledFeatureTest, CustomScheduleInvertedStartAndEndTimesCase3) {
   // Now is 11:00 PM.
-  test_clock_.SetNow(MakeTimeOfDay(11, AmPm::kPM).ToTimeToday());
+  test_clock()->SetNow(MakeTimeOfDay(11, AmPm::kPM).ToTimeToday());
   SetFeatureEnabled(false);
   // Start time is at 9:00 PM and end time is at 4:00 AM. "Now" is greater than
   // both. the feature should be on.
@@ -658,7 +642,7 @@
   //    2pm       4pm         7pm           10pm                         9am
   //
 
-  test_clock_.SetNow(MakeTimeOfDay(2, kPM).ToTimeToday());
+  test_clock()->SetNow(MakeTimeOfDay(2, kPM).ToTimeToday());
   feature()->SetCustomStartTime(MakeTimeOfDay(3, kPM));
   feature()->SetCustomEndTime(MakeTimeOfDay(8, kPM));
   feature()->SetScheduleType(ScheduledFeature::ScheduleType::kCustom);
@@ -688,7 +672,7 @@
 
     // Apply the test's case fake time, and fire the timer if there's a change
     // expected in the feature's status.
-    test_clock_.SetNow(test_case.fake_now);
+    test_clock()->SetNow(test_case.fake_now);
     if (user_1_previous_status != test_case.user_1_expected_status)
       feature()->timer()->FireNow();
     user_1_previous_status = test_case.user_1_expected_status;
@@ -730,7 +714,7 @@
 
 TEST_F(ScheduledFeatureTest,
        ManualStatusToggleCanPersistAfterResumeFromSuspend) {
-  test_clock_.SetNow(MakeTimeOfDay(11, kAM).ToTimeToday());
+  test_clock()->SetNow(MakeTimeOfDay(11, kAM).ToTimeToday());
 
   feature()->SetCustomStartTime(MakeTimeOfDay(3, kPM));
   feature()->SetCustomEndTime(MakeTimeOfDay(8, kPM));
@@ -746,18 +730,18 @@
 
   // Simulate suspend and then resume at 2:00 PM (which is outside the user's
   // custom schedule). However, the manual toggle to on should be kept.
-  test_clock_.SetNow(MakeTimeOfDay(2, kPM).ToTimeToday());
+  test_clock()->SetNow(MakeTimeOfDay(2, kPM).ToTimeToday());
   feature()->SuspendDone(base::TimeDelta{});
   EXPECT_TRUE(feature()->GetEnabled());
 
   // Suspend again and resume at 5:00 PM (which is within the user's custom
   // schedule). The schedule should be applied normally.
-  test_clock_.SetNow(MakeTimeOfDay(5, kPM).ToTimeToday());
+  test_clock()->SetNow(MakeTimeOfDay(5, kPM).ToTimeToday());
   feature()->SuspendDone(base::TimeDelta{});
   EXPECT_TRUE(feature()->GetEnabled());
 
   // Suspend and resume at 9:00 PM and expect the feature to be off.
-  test_clock_.SetNow(MakeTimeOfDay(9, kPM).ToTimeToday());
+  test_clock()->SetNow(MakeTimeOfDay(9, kPM).ToTimeToday());
   feature()->SuspendDone(base::TimeDelta{});
   EXPECT_FALSE(feature()->GetEnabled());
 }
diff --git a/ash/test_shell_delegate.cc b/ash/test_shell_delegate.cc
index bbcda14..0f0dc61 100644
--- a/ash/test_shell_delegate.cc
+++ b/ash/test_shell_delegate.cc
@@ -53,7 +53,7 @@
 }
 
 scoped_refptr<network::SharedURLLoaderFactory>
-TestShellDelegate::GetGeolocationSharedURLLoaderFactory() const {
+TestShellDelegate::GetGeolocationUrlLoaderFactory() const {
   return static_cast<scoped_refptr<network::SharedURLLoaderFactory>>(
       base::MakeRefCounted<TestGeolocationUrlLoaderFactory>());
 }
diff --git a/ash/test_shell_delegate.h b/ash/test_shell_delegate.h
index 10afec5..d2619f40 100644
--- a/ash/test_shell_delegate.h
+++ b/ash/test_shell_delegate.h
@@ -45,7 +45,7 @@
   std::unique_ptr<DesksTemplatesDelegate> CreateDesksTemplatesDelegate()
       const override;
   scoped_refptr<network::SharedURLLoaderFactory>
-  GetGeolocationSharedURLLoaderFactory() const override;
+  GetGeolocationUrlLoaderFactory() const override;
   bool CanGoBack(gfx::NativeWindow window) const override;
   void SetTabScrubberChromeOSEnabled(bool enabled) override;
   bool ShouldWaitForTouchPressAck(gfx::NativeWindow window) override;
diff --git a/ash/webui/eche_app_ui/BUILD.gn b/ash/webui/eche_app_ui/BUILD.gn
index dcb6a24..7d8921d 100644
--- a/ash/webui/eche_app_ui/BUILD.gn
+++ b/ash/webui/eche_app_ui/BUILD.gn
@@ -37,8 +37,6 @@
     "eche_connector.h",
     "eche_connector_impl.cc",
     "eche_connector_impl.h",
-    "eche_display_stream_handler.cc",
-    "eche_display_stream_handler.h",
     "eche_feature_status_provider.cc",
     "eche_feature_status_provider.h",
     "eche_message_receiver.cc",
@@ -55,6 +53,10 @@
     "eche_recent_app_click_handler.h",
     "eche_signaler.cc",
     "eche_signaler.h",
+    "eche_stream_status_change_handler.cc",
+    "eche_stream_status_change_handler.h",
+    "eche_tray_stream_status_observer.cc",
+    "eche_tray_stream_status_observer.h",
     "eche_uid_provider.cc",
     "eche_uid_provider.h",
     "feature_status.cc",
@@ -128,7 +130,6 @@
     "apps_access_setup_operation_unittest.cc",
     "eche_app_manager_unittest.cc",
     "eche_connector_impl_unittest.cc",
-    "eche_display_stream_handler_unittest.cc",
     "eche_feature_status_provider_unittest.cc",
     "eche_message_receiver_impl_unittest.cc",
     "eche_notification_click_handler_unittest.cc",
@@ -136,6 +137,8 @@
     "eche_presence_manager_unittest.cc",
     "eche_recent_app_click_handler_unittest.cc",
     "eche_signaler_unittest.cc",
+    "eche_stream_status_change_handler_unittest.cc",
+    "eche_tray_stream_status_observer_unittest.cc",
     "eche_uid_provider_unittest.cc",
     "launch_app_helper_unittest.cc",
     "system_info_provider_unittest.cc",
diff --git a/ash/webui/eche_app_ui/eche_app_manager.cc b/ash/webui/eche_app_ui/eche_app_manager.cc
index ef1737ab..a6633fb 100644
--- a/ash/webui/eche_app_ui/eche_app_manager.cc
+++ b/ash/webui/eche_app_ui/eche_app_manager.cc
@@ -5,15 +5,17 @@
 #include "ash/webui/eche_app_ui/eche_app_manager.h"
 
 #include "ash/components/phonehub/phone_hub_manager.h"
+#include "ash/constants/ash_features.h"
 #include "ash/public/cpp/network_config_service.h"
 #include "ash/services/secure_channel/public/cpp/client/connection_manager_impl.h"
 #include "ash/webui/eche_app_ui/apps_access_manager_impl.h"
 #include "ash/webui/eche_app_ui/eche_connector_impl.h"
-#include "ash/webui/eche_app_ui/eche_display_stream_handler.h"
 #include "ash/webui/eche_app_ui/eche_message_receiver_impl.h"
 #include "ash/webui/eche_app_ui/eche_notification_generator.h"
 #include "ash/webui/eche_app_ui/eche_presence_manager.h"
 #include "ash/webui/eche_app_ui/eche_signaler.h"
+#include "ash/webui/eche_app_ui/eche_stream_status_change_handler.h"
+#include "ash/webui/eche_app_ui/eche_tray_stream_status_observer.h"
 #include "ash/webui/eche_app_ui/eche_uid_provider.h"
 #include "ash/webui/eche_app_ui/launch_app_helper.h"
 #include "ash/webui/eche_app_ui/system_info.h"
@@ -40,8 +42,7 @@
         presence_monitor_client,
     LaunchAppHelper::LaunchEcheAppFunction launch_eche_app_function,
     LaunchAppHelper::CloseEcheAppFunction close_eche_app_function,
-    LaunchAppHelper::LaunchNotificationFunction launch_notification_function,
-    StreamStatusChangedFunction stream_status_changed_function)
+    LaunchAppHelper::LaunchNotificationFunction launch_notification_function)
     : connection_manager_(
           std::make_unique<secure_channel::ConnectionManagerImpl>(
               multidevice_setup_client,
@@ -61,7 +62,8 @@
                                             launch_eche_app_function,
                                             close_eche_app_function,
                                             launch_notification_function)),
-      display_stream_handler_(std::make_unique<EcheDisplayStreamHandler>()),
+      stream_status_change_handler_(
+          std::make_unique<EcheStreamStatusChangeHandler>()),
       eche_notification_click_handler_(
           std::make_unique<EcheNotificationClickHandler>(
               phone_hub_manager,
@@ -96,13 +98,15 @@
           pref_service,
           multidevice_setup_client,
           connection_manager_.get())),
-      stream_status_changed_function_(
-          std::move(stream_status_changed_function)) {
+      eche_tray_stream_status_observer_(
+          features::IsEcheCustomWidgetEnabled()
+              ? std::make_unique<EcheTrayStreamStatusObserver>(
+                    stream_status_change_handler_.get())
+              : nullptr) {
   ash::GetNetworkConfigService(
       remote_cros_network_config_.BindNewPipeAndPassReceiver());
   system_info_provider_ = std::make_unique<SystemInfoProvider>(
       std::move(system_info), remote_cros_network_config_.get());
-  display_stream_handler_->AddObserver(this);
 }
 
 EcheAppManager::~EcheAppManager() = default;
@@ -129,16 +133,7 @@
 
 void EcheAppManager::BindDisplayStreamHandlerInterface(
     mojo::PendingReceiver<mojom::DisplayStreamHandler> receiver) {
-  display_stream_handler_->Bind(std::move(receiver));
-}
-
-void EcheAppManager::OnStartStreaming() {
-  stream_status_changed_function_.Run(
-      mojom::StreamStatus::kStreamStatusStarted);
-}
-
-void EcheAppManager::OnStreamStatusChanged(mojom::StreamStatus status) {
-  stream_status_changed_function_.Run(status);
+  stream_status_change_handler_->Bind(std::move(receiver));
 }
 
 AppsAccessManager* EcheAppManager::GetAppsAccessManager() {
@@ -149,6 +144,7 @@
 // are initialized in the constructor.
 void EcheAppManager::Shutdown() {
   system_info_provider_.reset();
+  eche_tray_stream_status_observer_.reset();
   apps_access_manager_.reset();
   notification_generator_.reset();
   eche_recent_app_click_handler_.reset();
@@ -158,8 +154,7 @@
   signaler_.reset();
   eche_connector_.reset();
   eche_notification_click_handler_.reset();
-  display_stream_handler_->RemoveObserver(this);
-  display_stream_handler_.reset();
+  stream_status_change_handler_.reset();
   launch_app_helper_.reset();
   feature_status_provider_.reset();
   connection_manager_.reset();
diff --git a/ash/webui/eche_app_ui/eche_app_manager.h b/ash/webui/eche_app_ui/eche_app_manager.h
index e91fde0..b9c31c0a 100644
--- a/ash/webui/eche_app_ui/eche_app_manager.h
+++ b/ash/webui/eche_app_ui/eche_app_manager.h
@@ -14,7 +14,6 @@
 #include "ash/services/secure_channel/public/cpp/client/presence_monitor_client_impl.h"
 // TODO(https://crbug.com/1164001): move to forward declaration.
 #include "ash/services/secure_channel/public/cpp/client/secure_channel_client.h"
-#include "ash/webui/eche_app_ui/eche_display_stream_handler.h"
 #include "ash/webui/eche_app_ui/eche_feature_status_provider.h"
 #include "ash/webui/eche_app_ui/eche_notification_click_handler.h"
 #include "ash/webui/eche_app_ui/eche_recent_app_click_handler.h"
@@ -47,19 +46,14 @@
 class SystemInfo;
 class SystemInfoProvider;
 class AppsAccessManager;
-class EcheDisplayStreamHandler;
+class EcheStreamStatusChangeHandler;
+class EcheTrayStreamStatusObserver;
 
 // Implements the core logic of the EcheApp and exposes interfaces via its
 // public API. Implemented as a KeyedService since it depends on other
 // KeyedService instances.
-class EcheAppManager : public KeyedService,
-                       public EcheDisplayStreamHandler::Observer {
+class EcheAppManager : public KeyedService {
  public:
-  using StreamStatusChangedFunction =
-      base::RepeatingCallback<void(const mojom::StreamStatus status)>;
-
-  // TODO(b/223321926): clean up callback functions from constructor to a
-  // specific class
   EcheAppManager(PrefService* pref_service,
                  std::unique_ptr<SystemInfo> system_info,
                  phonehub::PhoneHubManager*,
@@ -70,8 +64,7 @@
                      presence_monitor_client,
                  LaunchAppHelper::LaunchEcheAppFunction,
                  LaunchAppHelper::CloseEcheAppFunction,
-                 LaunchAppHelper::LaunchNotificationFunction,
-                 StreamStatusChangedFunction);
+                 LaunchAppHelper::LaunchNotificationFunction);
   ~EcheAppManager() override;
 
   EcheAppManager(const EcheAppManager&) = delete;
@@ -97,15 +90,11 @@
   // KeyedService:
   void Shutdown() override;
 
-  // EcheDisplayStreamHandler::Observer:
-  void OnStartStreaming() override;
-  void OnStreamStatusChanged(mojom::StreamStatus status) override;
-
  private:
   std::unique_ptr<secure_channel::ConnectionManager> connection_manager_;
   std::unique_ptr<EcheFeatureStatusProvider> feature_status_provider_;
   std::unique_ptr<LaunchAppHelper> launch_app_helper_;
-  std::unique_ptr<EcheDisplayStreamHandler> display_stream_handler_;
+  std::unique_ptr<EcheStreamStatusChangeHandler> stream_status_change_handler_;
   std::unique_ptr<EcheNotificationClickHandler>
       eche_notification_click_handler_;
   std::unique_ptr<EcheConnector> eche_connector_;
@@ -119,7 +108,8 @@
       remote_cros_network_config_;
   std::unique_ptr<SystemInfoProvider> system_info_provider_;
   std::unique_ptr<AppsAccessManager> apps_access_manager_;
-  StreamStatusChangedFunction stream_status_changed_function_;
+  std::unique_ptr<EcheTrayStreamStatusObserver>
+      eche_tray_stream_status_observer_;
 };
 
 }  // namespace eche_app
diff --git a/ash/webui/eche_app_ui/eche_app_manager_unittest.cc b/ash/webui/eche_app_ui/eche_app_manager_unittest.cc
index c981523..17a4ca0 100644
--- a/ash/webui/eche_app_ui/eche_app_manager_unittest.cc
+++ b/ash/webui/eche_app_ui/eche_app_manager_unittest.cc
@@ -15,7 +15,7 @@
 #include "ash/services/secure_channel/public/cpp/client/fake_secure_channel_client.h"
 #include "ash/services/secure_channel/public/cpp/client/presence_monitor_client.h"
 #include "ash/services/secure_channel/public/cpp/client/presence_monitor_client_impl.h"
-#include "ash/webui/eche_app_ui/eche_display_stream_handler.h"
+#include "ash/webui/eche_app_ui/eche_stream_status_change_handler.h"
 #include "ash/webui/eche_app_ui/launch_app_helper.h"
 #include "ash/webui/eche_app_ui/system_info.h"
 #include "base/bind.h"
@@ -46,8 +46,6 @@
     const absl::optional<std::u16string>& message,
     std::unique_ptr<LaunchAppHelper::NotificationInfo> info) {}
 
-void StreamStatusChangedFunction(const mojom::StreamStatus status) {}
-
 class FakePresenceMonitorClient : public secure_channel::PresenceMonitorClient {
  public:
   FakePresenceMonitorClient() = default;
@@ -119,8 +117,7 @@
         std::move(fake_presence_monitor_client),
         base::BindRepeating(&LaunchEcheAppFunction),
         base::BindRepeating(&CloseEcheAppFunction),
-        base::BindRepeating(&LaunchNotificationFunction),
-        base::BindRepeating(&StreamStatusChangedFunction));
+        base::BindRepeating(&LaunchNotificationFunction));
   }
 
   mojo::Remote<mojom::SignalingMessageExchanger>&
diff --git a/ash/webui/eche_app_ui/eche_display_stream_handler.cc b/ash/webui/eche_app_ui/eche_display_stream_handler.cc
deleted file mode 100644
index 0618a38..0000000
--- a/ash/webui/eche_app_ui/eche_display_stream_handler.cc
+++ /dev/null
@@ -1,55 +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 "ash/webui/eche_app_ui/eche_display_stream_handler.h"
-
-#include "ash/components/multidevice/logging/logging.h"
-#include "ash/webui/eche_app_ui/launch_app_helper.h"
-
-namespace ash {
-namespace eche_app {
-
-EcheDisplayStreamHandler::EcheDisplayStreamHandler() = default;
-
-EcheDisplayStreamHandler::~EcheDisplayStreamHandler() = default;
-
-void EcheDisplayStreamHandler::StartStreaming() {
-  PA_LOG(INFO) << "echeapi EcheDisplayStreamHandler StartStreaming";
-  NotifyStartStreaming();
-}
-
-void EcheDisplayStreamHandler::OnStreamStatusChanged(
-    mojom::StreamStatus status) {
-  PA_LOG(INFO) << "echeapi EcheDisplayStreamHandler OnStreamStatusChanged "
-               << status;
-  NotifyStreamStatusChanged(status);
-}
-
-void EcheDisplayStreamHandler::Bind(
-    mojo::PendingReceiver<mojom::DisplayStreamHandler> receiver) {
-  display_stream_receiver_.reset();
-  display_stream_receiver_.Bind(std::move(receiver));
-}
-
-void EcheDisplayStreamHandler::AddObserver(Observer* observer) {
-  observer_list_.AddObserver(observer);
-}
-
-void EcheDisplayStreamHandler::RemoveObserver(Observer* observer) {
-  observer_list_.RemoveObserver(observer);
-}
-
-void EcheDisplayStreamHandler::NotifyStartStreaming() {
-  for (auto& observer : observer_list_)
-    observer.OnStartStreaming();
-}
-
-void EcheDisplayStreamHandler::NotifyStreamStatusChanged(
-    mojom::StreamStatus status) {
-  for (auto& observer : observer_list_)
-    observer.OnStreamStatusChanged(status);
-}
-
-}  // namespace eche_app
-}  // namespace ash
diff --git a/ash/webui/eche_app_ui/eche_display_stream_handler.h b/ash/webui/eche_app_ui/eche_display_stream_handler.h
deleted file mode 100644
index 0696119..0000000
--- a/ash/webui/eche_app_ui/eche_display_stream_handler.h
+++ /dev/null
@@ -1,59 +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 ASH_WEBUI_ECHE_APP_UI_ECHE_DISPLAY_STREAM_HANDLER_H_
-#define ASH_WEBUI_ECHE_APP_UI_ECHE_DISPLAY_STREAM_HANDLER_H_
-
-#include "ash/webui/eche_app_ui/mojom/eche_app.mojom.h"
-#include "base/observer_list.h"
-#include "base/observer_list_types.h"
-#include "mojo/public/cpp/bindings/receiver.h"
-
-namespace ash {
-namespace eche_app {
-
-// Implements the EcheDisplayStreamHandler interface to allow the WebUI to sync
-// the status of the display stream for Eche, e.g. When the display stream is
-// started  in the Eche Web, we can register `Observer` and get this status via
-// `OnStartStreaming` event.
-// TODO(paulzchen): Consider using `DisplayStreamEventHandler` to replace
-// `DisplayStreamHandler`.
-class EcheDisplayStreamHandler : public mojom::DisplayStreamHandler {
- public:
-  class Observer : public base::CheckedObserver {
-   public:
-    ~Observer() override = default;
-
-    virtual void OnStartStreaming() = 0;
-    virtual void OnStreamStatusChanged(mojom::StreamStatus status) = 0;
-  };
-
-  EcheDisplayStreamHandler();
-  ~EcheDisplayStreamHandler() override;
-
-  EcheDisplayStreamHandler(const EcheDisplayStreamHandler&) = delete;
-  EcheDisplayStreamHandler& operator=(const EcheDisplayStreamHandler&) = delete;
-
-  // mojom::DisplayStreamHandler:
-  void StartStreaming() override;
-  void OnStreamStatusChanged(mojom::StreamStatus status) override;
-
-  void AddObserver(Observer* observer);
-  void RemoveObserver(Observer* observer);
-
-  void Bind(mojo::PendingReceiver<mojom::DisplayStreamHandler> receiver);
-
- protected:
-  void NotifyStartStreaming();
-  void NotifyStreamStatusChanged(mojom::StreamStatus status);
-
- private:
-  mojo::Receiver<mojom::DisplayStreamHandler> display_stream_receiver_{this};
-  base::ObserverList<Observer> observer_list_;
-};
-
-}  // namespace eche_app
-}  // namespace ash
-
-#endif  // ASH_WEBUI_ECHE_APP_UI_ECHE_DISPLAY_STREAM_HANDLER_H_
diff --git a/ash/webui/eche_app_ui/eche_stream_status_change_handler.cc b/ash/webui/eche_app_ui/eche_stream_status_change_handler.cc
new file mode 100644
index 0000000..756f2a7
--- /dev/null
+++ b/ash/webui/eche_app_ui/eche_stream_status_change_handler.cc
@@ -0,0 +1,55 @@
+// 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 "ash/webui/eche_app_ui/eche_stream_status_change_handler.h"
+
+#include "ash/components/multidevice/logging/logging.h"
+#include "ash/webui/eche_app_ui/launch_app_helper.h"
+
+namespace ash {
+namespace eche_app {
+
+EcheStreamStatusChangeHandler::EcheStreamStatusChangeHandler() = default;
+
+EcheStreamStatusChangeHandler::~EcheStreamStatusChangeHandler() = default;
+
+void EcheStreamStatusChangeHandler::StartStreaming() {
+  PA_LOG(INFO) << "echeapi EcheStreamStatusChangeHandler StartStreaming";
+  NotifyStartStreaming();
+}
+
+void EcheStreamStatusChangeHandler::OnStreamStatusChanged(
+    mojom::StreamStatus status) {
+  PA_LOG(INFO) << "echeapi EcheStreamStatusChangeHandler OnStreamStatusChanged "
+               << status;
+  NotifyStreamStatusChanged(status);
+}
+
+void EcheStreamStatusChangeHandler::Bind(
+    mojo::PendingReceiver<mojom::DisplayStreamHandler> receiver) {
+  display_stream_receiver_.reset();
+  display_stream_receiver_.Bind(std::move(receiver));
+}
+
+void EcheStreamStatusChangeHandler::AddObserver(Observer* observer) {
+  observer_list_.AddObserver(observer);
+}
+
+void EcheStreamStatusChangeHandler::RemoveObserver(Observer* observer) {
+  observer_list_.RemoveObserver(observer);
+}
+
+void EcheStreamStatusChangeHandler::NotifyStartStreaming() {
+  for (auto& observer : observer_list_)
+    observer.OnStartStreaming();
+}
+
+void EcheStreamStatusChangeHandler::NotifyStreamStatusChanged(
+    mojom::StreamStatus status) {
+  for (auto& observer : observer_list_)
+    observer.OnStreamStatusChanged(status);
+}
+
+}  // namespace eche_app
+}  // namespace ash
diff --git a/ash/webui/eche_app_ui/eche_stream_status_change_handler.h b/ash/webui/eche_app_ui/eche_stream_status_change_handler.h
new file mode 100644
index 0000000..06ae6c4b
--- /dev/null
+++ b/ash/webui/eche_app_ui/eche_stream_status_change_handler.h
@@ -0,0 +1,58 @@
+// 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 ASH_WEBUI_ECHE_APP_UI_ECHE_STREAM_STATUS_CHANGE_HANDLER_H_
+#define ASH_WEBUI_ECHE_APP_UI_ECHE_STREAM_STATUS_CHANGE_HANDLER_H_
+
+#include "ash/webui/eche_app_ui/mojom/eche_app.mojom.h"
+#include "base/observer_list.h"
+#include "base/observer_list_types.h"
+#include "mojo/public/cpp/bindings/receiver.h"
+
+namespace ash {
+namespace eche_app {
+
+// Implements the DisplayStreamHandler interface to allow the WebUI to sync the
+// status of the video streaming for Eche, e.g. When the video streaming is
+// started in the Eche Web, we can register `Observer` and get this status via
+// `OnStartStreaming` and `OnStreamStatusChanged` event.
+class EcheStreamStatusChangeHandler : public mojom::DisplayStreamHandler {
+ public:
+  class Observer : public base::CheckedObserver {
+   public:
+    ~Observer() override = default;
+
+    virtual void OnStartStreaming() = 0;
+    virtual void OnStreamStatusChanged(mojom::StreamStatus status) = 0;
+  };
+
+  EcheStreamStatusChangeHandler();
+  ~EcheStreamStatusChangeHandler() override;
+
+  EcheStreamStatusChangeHandler(const EcheStreamStatusChangeHandler&) = delete;
+  EcheStreamStatusChangeHandler& operator=(
+      const EcheStreamStatusChangeHandler&) = delete;
+
+  // mojom::DisplayStreamHandler:
+  void StartStreaming() override;
+  void OnStreamStatusChanged(mojom::StreamStatus status) override;
+
+  void AddObserver(Observer* observer);
+  void RemoveObserver(Observer* observer);
+
+  void Bind(mojo::PendingReceiver<mojom::DisplayStreamHandler> receiver);
+
+ protected:
+  void NotifyStartStreaming();
+  void NotifyStreamStatusChanged(mojom::StreamStatus status);
+
+ private:
+  mojo::Receiver<mojom::DisplayStreamHandler> display_stream_receiver_{this};
+  base::ObserverList<Observer> observer_list_;
+};
+
+}  // namespace eche_app
+}  // namespace ash
+
+#endif  // ASH_WEBUI_ECHE_APP_UI_ECHE_STREAM_STATUS_CHANGE_HANDLER_H_
diff --git a/ash/webui/eche_app_ui/eche_display_stream_handler_unittest.cc b/ash/webui/eche_app_ui/eche_stream_status_change_handler_unittest.cc
similarity index 73%
rename from ash/webui/eche_app_ui/eche_display_stream_handler_unittest.cc
rename to ash/webui/eche_app_ui/eche_stream_status_change_handler_unittest.cc
index 4db83d6..50575bc 100644
--- a/ash/webui/eche_app_ui/eche_display_stream_handler_unittest.cc
+++ b/ash/webui/eche_app_ui/eche_stream_status_change_handler_unittest.cc
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#include "ash/webui/eche_app_ui/eche_display_stream_handler.h"
+#include "ash/webui/eche_app_ui/eche_stream_status_change_handler.h"
 
 #include "testing/gtest/include/gtest/gtest.h"
 
@@ -10,7 +10,7 @@
 namespace eche_app {
 namespace {
 
-class FakeObserver : public EcheDisplayStreamHandler::Observer {
+class FakeObserver : public EcheStreamStatusChangeHandler::Observer {
  public:
   FakeObserver() = default;
   ~FakeObserver() override = default;
@@ -22,7 +22,7 @@
     return last_notified_stream_status_;
   }
 
-  // EcheDisplayStreamHandler::Observer:
+  // EcheStreamStatusChangeHandler::Observer:
   void OnStartStreaming() override { ++num_start_streaming_calls_; }
   void OnStreamStatusChanged(mojom::StreamStatus status) override {
     last_notified_stream_status_ = status;
@@ -36,17 +36,18 @@
 
 }  // namespace
 
-class EcheDisplayStreamHandlerTest : public testing::Test {
+class EcheStreamStatusChangeHandlerTest : public testing::Test {
  protected:
-  EcheDisplayStreamHandlerTest() = default;
-  EcheDisplayStreamHandlerTest(const EcheDisplayStreamHandlerTest&) = delete;
-  EcheDisplayStreamHandlerTest& operator=(const EcheDisplayStreamHandlerTest&) =
+  EcheStreamStatusChangeHandlerTest() = default;
+  EcheStreamStatusChangeHandlerTest(const EcheStreamStatusChangeHandlerTest&) =
       delete;
-  ~EcheDisplayStreamHandlerTest() override = default;
+  EcheStreamStatusChangeHandlerTest& operator=(
+      const EcheStreamStatusChangeHandlerTest&) = delete;
+  ~EcheStreamStatusChangeHandlerTest() override = default;
 
   // testing::Test:
   void SetUp() override {
-    handler_ = std::make_unique<EcheDisplayStreamHandler>();
+    handler_ = std::make_unique<EcheStreamStatusChangeHandler>();
     handler_->AddObserver(&fake_observer_);
   }
 
@@ -69,15 +70,15 @@
 
  private:
   FakeObserver fake_observer_;
-  std::unique_ptr<EcheDisplayStreamHandler> handler_;
+  std::unique_ptr<EcheStreamStatusChangeHandler> handler_;
 };
 
-TEST_F(EcheDisplayStreamHandlerTest, StartStreaming) {
+TEST_F(EcheStreamStatusChangeHandlerTest, StartStreaming) {
   StartStreaming();
   EXPECT_EQ(1u, GetNumObserverStartStreamingCalls());
 }
 
-TEST_F(EcheDisplayStreamHandlerTest, OnStreamStatusChanged) {
+TEST_F(EcheStreamStatusChangeHandlerTest, OnStreamStatusChanged) {
   NotifyStreamStatus(mojom::StreamStatus::kStreamStatusInitializing);
   EXPECT_EQ(mojom::StreamStatus::kStreamStatusInitializing,
             GetObservedStreamStatus());
diff --git a/ash/webui/eche_app_ui/eche_tray_stream_status_observer.cc b/ash/webui/eche_app_ui/eche_tray_stream_status_observer.cc
new file mode 100644
index 0000000..fc0fadb7
--- /dev/null
+++ b/ash/webui/eche_app_ui/eche_tray_stream_status_observer.cc
@@ -0,0 +1,53 @@
+// 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 "ash/webui/eche_app_ui/eche_tray_stream_status_observer.h"
+
+#include "ash/root_window_controller.h"
+#include "ash/shell.h"
+#include "ash/system/eche/eche_tray.h"
+#include "ash/webui/eche_app_ui/eche_stream_status_change_handler.h"
+#include "ui/gfx/image/image.h"
+
+namespace ash {
+
+EcheTray* GetEcheTray() {
+  return Shell::GetPrimaryRootWindowController()
+      ->GetStatusAreaWidget()
+      ->eche_tray();
+}
+
+namespace eche_app {
+
+void LaunchBubble(const GURL& url, const gfx::Image& icon) {
+  auto* eche_tray = ash::GetEcheTray();
+  DCHECK(eche_tray);
+  eche_tray->LoadBubble(url, icon);
+}
+
+void CloseBubble() {
+  auto* eche_tray = ash::GetEcheTray();
+  if (eche_tray)
+    eche_tray->PurgeAndClose();
+  return;
+}
+
+EcheTrayStreamStatusObserver::EcheTrayStreamStatusObserver(
+    EcheStreamStatusChangeHandler* stream_status_change_handler) {
+  observed_session_.Observe(stream_status_change_handler);
+}
+
+EcheTrayStreamStatusObserver::~EcheTrayStreamStatusObserver() = default;
+
+void EcheTrayStreamStatusObserver::OnStartStreaming() {
+  OnStreamStatusChanged(mojom::StreamStatus::kStreamStatusStarted);
+}
+
+void EcheTrayStreamStatusObserver::OnStreamStatusChanged(
+    mojom::StreamStatus status) {
+  GetEcheTray()->OnStreamStatusChanged(status);
+}
+
+}  // namespace eche_app
+}  // namespace ash
diff --git a/ash/webui/eche_app_ui/eche_tray_stream_status_observer.h b/ash/webui/eche_app_ui/eche_tray_stream_status_observer.h
new file mode 100644
index 0000000..b560b8b
--- /dev/null
+++ b/ash/webui/eche_app_ui/eche_tray_stream_status_observer.h
@@ -0,0 +1,56 @@
+// 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 ASH_WEBUI_ECHE_APP_UI_ECHE_TRAY_STREAM_STATUS_OBSERVER_H_
+#define ASH_WEBUI_ECHE_APP_UI_ECHE_TRAY_STREAM_STATUS_OBSERVER_H_
+
+#include "ash/webui/eche_app_ui/eche_stream_status_change_handler.h"
+#include "ash/webui/eche_app_ui/mojom/eche_app.mojom.h"
+#include "base/scoped_observation.h"
+#include "url/gurl.h"
+
+namespace gfx {
+class Image;
+}  // namespace gfx
+
+namespace ash {
+namespace eche_app {
+
+// It is called from chrome/browser/ash/eche_app/eche_app_manager_factory.cc.
+// Move all logic about Eche tray to here because we don't want to call
+// `GetEcheTray` everywhere.
+void LaunchBubble(const GURL& url, const gfx::Image& icon);
+
+// It is called from chrome/browser/ash/eche_app/eche_app_manager_factory.cc.
+void CloseBubble();
+
+// The observer that observes the stream status change and notifies `EcheTray`
+// show/hide/close the bubble when Eche starts/stops streaming.
+// TODO(b/226687249): Implement this observer in EcheTray directly if we fix the
+// package dependency error between //eche_app_ui and //ash.
+class EcheTrayStreamStatusObserver
+    : public EcheStreamStatusChangeHandler::Observer {
+ public:
+  EcheTrayStreamStatusObserver(
+      EcheStreamStatusChangeHandler* stream_status_change_handler);
+  ~EcheTrayStreamStatusObserver() override;
+
+  EcheTrayStreamStatusObserver(const EcheTrayStreamStatusObserver&) = delete;
+  EcheTrayStreamStatusObserver& operator=(const EcheTrayStreamStatusObserver&) =
+      delete;
+
+  // EcheStreamStatusChangeHandler::Observer:
+  void OnStartStreaming() override;
+  void OnStreamStatusChanged(mojom::StreamStatus status) override;
+
+ private:
+  base::ScopedObservation<EcheStreamStatusChangeHandler,
+                          EcheStreamStatusChangeHandler::Observer>
+      observed_session_{this};
+};
+
+}  // namespace eche_app
+}  // namespace ash
+
+#endif  // ASH_WEBUI_ECHE_APP_UI_ECHE_TRAY_STREAM_STATUS_OBSERVER_H_
diff --git a/ash/webui/eche_app_ui/eche_tray_stream_status_observer_unittest.cc b/ash/webui/eche_app_ui/eche_tray_stream_status_observer_unittest.cc
new file mode 100644
index 0000000..8f332ca
--- /dev/null
+++ b/ash/webui/eche_app_ui/eche_tray_stream_status_observer_unittest.cc
@@ -0,0 +1,133 @@
+// 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 "ash/webui/eche_app_ui/eche_tray_stream_status_observer.h"
+
+#include "ash/constants/ash_features.h"
+#include "ash/system/eche/eche_tray.h"
+#include "ash/system/status_area_widget_test_helper.h"
+#include "ash/test/ash_test_base.h"
+#include "ash/test/ash_test_suite.h"
+#include "ash/test/test_ash_web_view_factory.h"
+#include "ash/webui/eche_app_ui/eche_stream_status_change_handler.h"
+#include "base/test/scoped_feature_list.h"
+#include "ui/base/resource/resource_bundle.h"
+#include "ui/gfx/image/image.h"
+
+namespace ash {
+namespace eche_app {
+
+class EcheTrayStreamStatusObserverTest : public AshTestBase {
+ protected:
+  EcheTrayStreamStatusObserverTest() = default;
+  EcheTrayStreamStatusObserverTest(const EcheTrayStreamStatusObserverTest&) =
+      delete;
+  EcheTrayStreamStatusObserverTest& operator=(
+      const EcheTrayStreamStatusObserverTest&) = delete;
+  ~EcheTrayStreamStatusObserverTest() override = default;
+
+  // AshTestBase:
+  void SetUp() override {
+    scoped_feature_list_.InitWithFeatures(
+        /*enabled_features=*/{},
+        /*disabled_features=*/{features::kEcheSWAInBackground});
+    DCHECK(test_web_view_factory_.get());
+    ui::ResourceBundle::CleanupSharedInstance();
+    AshTestSuite::LoadTestResources();
+    AshTestBase::SetUp();
+    eche_tray_ =
+        ash::StatusAreaWidgetTestHelper::GetStatusAreaWidget()->eche_tray();
+
+    stream_status_change_handler_ =
+        std::make_unique<EcheStreamStatusChangeHandler>();
+    observer_ = std::make_unique<EcheTrayStreamStatusObserver>(
+        stream_status_change_handler_.get());
+  }
+
+  void TearDown() override {
+    observer_.reset();
+    stream_status_change_handler_.reset();
+    AshTestBase::TearDown();
+  }
+
+  void OnStartStreaming() { observer_->OnStartStreaming(); }
+
+  void OnStreamStatusChanged(mojom::StreamStatus status) {
+    observer_->OnStreamStatusChanged(status);
+  }
+
+  EcheTray* eche_tray() { return eche_tray_; }
+
+ private:
+  base::test::ScopedFeatureList scoped_feature_list_;
+  EcheTray* eche_tray_ = nullptr;
+  std::unique_ptr<EcheStreamStatusChangeHandler> stream_status_change_handler_;
+  std::unique_ptr<EcheTrayStreamStatusObserver> observer_;
+  // Calling the factory constructor is enough to set it up.
+  std::unique_ptr<TestAshWebViewFactory> test_web_view_factory_ =
+      std::make_unique<TestAshWebViewFactory>();
+};
+
+TEST_F(EcheTrayStreamStatusObserverTest, LaunchBubble) {
+  LaunchBubble(GURL("http://google.com"), gfx::Image());
+
+  // Wait for Eche Tray to load Eche Web to complete.
+  base::RunLoop().RunUntilIdle();
+  // Bubble widget should be created after launch.
+  EXPECT_TRUE(eche_tray()->get_bubble_wrapper_for_test());
+}
+
+TEST_F(EcheTrayStreamStatusObserverTest, CloseBubble) {
+  CloseBubble();
+
+  // Wait for Eche Web to close.
+  base::RunLoop().RunUntilIdle();
+  // Eche tray should be visible after close.
+  EXPECT_FALSE(eche_tray()->is_active());
+}
+
+TEST_F(EcheTrayStreamStatusObserverTest, OnStartStreaming) {
+  OnStartStreaming();
+
+  // Wait for Eche Tray to load Eche Web to complete.
+  base::RunLoop().RunUntilIdle();
+  // The bubble should not be created if LaunchBubble be called before.
+  EXPECT_FALSE(eche_tray()->get_bubble_wrapper_for_test());
+
+  LaunchBubble(GURL("http://google.com"), gfx::Image());
+
+  // Wait for Eche Tray to load Eche Web to complete.
+  base::RunLoop().RunUntilIdle();
+  // The bubble widget should be created but not be activated yet.
+  EXPECT_TRUE(eche_tray()->get_bubble_wrapper_for_test());
+  EXPECT_FALSE(eche_tray()->is_active());
+
+  OnStartStreaming();
+
+  // Wait for the bubble to show up.
+  base::RunLoop().RunUntilIdle();
+  // The bubble widget should be activated.
+  EXPECT_TRUE(eche_tray()->get_bubble_wrapper_for_test());
+  EXPECT_TRUE(eche_tray()->is_active());
+}
+
+TEST_F(EcheTrayStreamStatusObserverTest, OnStreamStatusChanged) {
+  LaunchBubble(GURL("http://google.com"), gfx::Image());
+  OnStreamStatusChanged(mojom::StreamStatus::kStreamStatusStarted);
+
+  // Wait for Eche Tray to load Eche Web to complete.
+  base::RunLoop().RunUntilIdle();
+  // Eche tray should be visible when streaming is active
+  EXPECT_TRUE(eche_tray()->get_bubble_wrapper_for_test());
+
+  OnStreamStatusChanged(mojom::StreamStatus::kStreamStatusStopped);
+
+  // Wait for Eche Web to close.
+  base::RunLoop().RunUntilIdle();
+  // Eche tray should not be visible when streaming is finished
+  EXPECT_FALSE(eche_tray()->is_active());
+}
+
+}  // namespace eche_app
+}  // namespace ash
diff --git a/ash/webui/personalization_app/README.md b/ash/webui/personalization_app/README.md
index ea79185..da9bba1 100644
--- a/ash/webui/personalization_app/README.md
+++ b/ash/webui/personalization_app/README.md
@@ -6,20 +6,20 @@
 - Follow https://chromium.googlesource.com/chromium/src/+/HEAD/docs/vscode.md
 - Config `tsconfig.json`:
   - Create or update `${PATH_TO_CHROMIUM}/src/ash/webui/personalization_app/resources/tsconfig.json`
+    - Remember to replace `out/${YOUR_BUILD}` with your out directory.
   ```
   {
     "__comment__": [
         "This file is used by local typescript language server. It is manually",
         "maintained to be close to the corresponding ts_library() target in BUILD.gn. ",
-        "Be sure to replace locally 'out/Debug' with your out dir if it",
-        "is different. Or change your out dir to 'out/Debug'."
+        "Be sure to replace locally 'out/${YOUR_BUILD}' with your out dir"
     ],
     "extends": "./tsconfig_base.json",
     "compilerOptions": {
         "composite": true,
         "rootDirs": [
             ".",
-            "../../../../out/Debug/gen/ash/webui/personalization_app/resources/preprocessed"
+            "../../../../out/${YOUR_BUILD}/gen/ash/webui/personalization_app/resources/preprocessed"
         ],
         "moduleResolution": "node",
         "noEmit": true,
@@ -28,10 +28,10 @@
                 "./*"
             ],
             "chrome://resources/*": [
-                "../../../../out/Debug/gen/ui/webui/resources/preprocessed/*"
+                "../../../../out/${YOUR_BUILD}/gen/ui/webui/resources/preprocessed/*"
             ],
             "//resources/*": [
-                "../../../../out/Debug/gen/ui/webui/resources/preprocessed/*"
+                "../../../../out/${YOUR_BUILD}/gen/ui/webui/resources/preprocessed/*"
             ],
             "chrome://resources/polymer/v3_0/*": [
                 "../../../../third_party/polymer/v3_0/components-chromium/*"
@@ -70,14 +70,14 @@
     "compilerOptions": {
         "rootDirs": [
             ".",
-            "../../../../../../out/Debug/gen/chrome/test/data/webui/chromeos/personalization_app"
+            "../../../../../../out/${YOUR_BUILD}/gen/chrome/test/data/webui/chromeos/personalization_app"
         ],
         "paths": {
             "chrome://resources/*": [
-                "../../../../../../out/Debug/gen/ui/webui/resources/preprocessed/*"
+                "../../../../../../out/${YOUR_BUILD}/gen/ui/webui/resources/preprocessed/*"
             ],
             "//resources/*": [
-                "../../../../../../out/Debug/gen/ui/webui/resources/preprocessed/*"
+                "../../../../../../out/${YOUR_BUILD}/gen/ui/webui/resources/preprocessed/*"
             ],
             "chrome://resources/polymer/v3_0/*": [
                 "../../../../../../third_party/polymer/v3_0/components-chromium/*"
@@ -95,10 +95,10 @@
                 "../../../../../../tools/typescript/definitions/*"
             ],
             "chrome://personalization/*": [
-                "../../../../../../out/Debug/gen/ash/webui/personalization_app/resources/tsc/*"
+                "../../../../../../out/${YOUR_BUILD}/gen/ash/webui/personalization_app/resources/tsc/*"
             ],
             "chrome://webui-test/*": [
-                "../../../../../../out/Debug/gen/chrome/test/data/webui/tsc/*"
+                "../../../../../../out/${YOUR_BUILD}/gen/chrome/test/data/webui/tsc/*"
             ]
         }
     },
diff --git a/ash/wm/desks/desks_textfield.cc b/ash/wm/desks/desks_textfield.cc
index 2771e68..31ec315 100644
--- a/ash/wm/desks/desks_textfield.cc
+++ b/ash/wm/desks/desks_textfield.cc
@@ -171,6 +171,10 @@
 }
 
 SkColor DesksTextfield::GetBackgroundColor() const {
+  // Admin desk templates may be read only.
+  if (GetReadOnly())
+    return SK_ColorTRANSPARENT;
+
   return HasFocus() || IsMouseHovered()
              ? AshColorProvider::Get()->GetControlsLayerColor(
                    AshColorProvider::ControlsLayerType::
diff --git a/ash/wm/desks/templates/desks_templates_item_view.cc b/ash/wm/desks/templates/desks_templates_item_view.cc
index 3fe47bb9..8e7a2ce 100644
--- a/ash/wm/desks/templates/desks_templates_item_view.cc
+++ b/ash/wm/desks/templates/desks_templates_item_view.cc
@@ -155,7 +155,13 @@
                               .SetController(this)
                               .SetText(template_name)
                               .SetAccessibleName(template_name)
-                              .SetReadOnly(!desk_template_->IsModifiable()),
+                              .SetReadOnly(!desk_template_->IsModifiable())
+                              // Use the focus behavior specified by the
+                              // subclass of `DesksTemplatesNameView` unless the
+                              // template is not modifiable.
+                              .SetFocusBehavior(desk_template_->IsModifiable()
+                                                    ? GetFocusBehavior()
+                                                    : FocusBehavior::NEVER),
                           views::Builder<views::ImageView>()
                               .SetPreferredSize(
                                   gfx::Size(kManagedStatusIndicatorSize,
@@ -204,15 +210,19 @@
       l10n_util::GetStringUTF16(IDS_ASH_DESKS_TEMPLATES_USE_TEMPLATE_BUTTON),
       PillButton::Type::kIconless, /*icon=*/nullptr));
 
-  delete_button_ = hover_container_->AddChildView(std::make_unique<CloseButton>(
-      base::BindRepeating(&DesksTemplatesItemView::OnDeleteButtonPressed,
-                          weak_ptr_factory_.GetWeakPtr()),
-      CloseButton::Type::kMedium));
-  delete_button_->SetVectorIcon(kDeleteIcon);
-  delete_button_->SetTooltipText(l10n_util::GetStringUTF16(
-      IDS_ASH_DESKS_TEMPLATES_DELETE_DIALOG_CONFIRM_BUTTON));
-  delete_button_->SetBackgroundColor(color_provider->GetControlsLayerColor(
-      AshColorProvider::ControlsLayerType::kControlBackgroundColorInactive));
+  // Users cannot delete admin templates.
+  if (!is_admin_managed) {
+    delete_button_ =
+        hover_container_->AddChildView(std::make_unique<CloseButton>(
+            base::BindRepeating(&DesksTemplatesItemView::OnDeleteButtonPressed,
+                                weak_ptr_factory_.GetWeakPtr()),
+            CloseButton::Type::kMedium));
+    delete_button_->SetVectorIcon(kDeleteIcon);
+    delete_button_->SetTooltipText(l10n_util::GetStringUTF16(
+        IDS_ASH_DESKS_TEMPLATES_DELETE_DIALOG_CONFIRM_BUTTON));
+    delete_button_->SetBackgroundColor(color_provider->GetControlsLayerColor(
+        AshColorProvider::ControlsLayerType::kControlBackgroundColorInactive));
+  }
 
   name_view_observation_.Observe(name_view_);
 
@@ -316,12 +326,14 @@
   if (previous_name_view_width != name_view_->width())
     OnTemplateNameChanged(desk_template_->template_name());
 
-  const gfx::Size delete_button_size = delete_button_->GetPreferredSize();
-  DCHECK_EQ(delete_button_size.width(), delete_button_size.height());
-  delete_button_->SetBoundsRect(
-      gfx::Rect(width() - delete_button_size.width() - kDeleteButtonMargin,
-                kDeleteButtonMargin, delete_button_size.width(),
-                delete_button_size.height()));
+  if (delete_button_) {
+    const gfx::Size delete_button_size = delete_button_->GetPreferredSize();
+    DCHECK_EQ(delete_button_size.width(), delete_button_size.height());
+    delete_button_->SetBoundsRect(
+        gfx::Rect(width() - delete_button_size.width() - kDeleteButtonMargin,
+                  kDeleteButtonMargin, delete_button_size.width(),
+                  delete_button_size.height()));
+  }
 
   const gfx::Size launch_button_preferred_size =
       launch_button_->CalculatePreferredSize();
diff --git a/ash/wm/desks/templates/desks_templates_unittest.cc b/ash/wm/desks/templates/desks_templates_unittest.cc
index b7570eb..a59674f 100644
--- a/ash/wm/desks/templates/desks_templates_unittest.cc
+++ b/ash/wm/desks/templates/desks_templates_unittest.cc
@@ -86,12 +86,18 @@
   // for testing the names and times of the UI directly.
   void AddEntry(const base::GUID& uuid,
                 const std::string& name,
+                base::Time created_time) {
+    AddEntry(uuid, name, created_time, DeskTemplateSource::kUser,
+             std::make_unique<app_restore::RestoreData>());
+  }
+
+  void AddEntry(const base::GUID& uuid,
+                const std::string& name,
                 base::Time created_time,
-                std::unique_ptr<app_restore::RestoreData> restore_data =
-                    std::make_unique<app_restore::RestoreData>()) {
+                DeskTemplateSource source,
+                std::unique_ptr<app_restore::RestoreData> restore_data) {
     auto desk_template = std::make_unique<DeskTemplate>(
-        uuid.AsLowercaseString(), DeskTemplateSource::kUser, name,
-        created_time);
+        uuid.AsLowercaseString(), source, name, created_time);
     desk_template->set_desk_restore_data(std::move(restore_data));
 
     AddEntry(std::move(desk_template));
@@ -1021,6 +1027,7 @@
 TEST_F(DesksTemplatesTest, IconsOrder) {
   // Create a `DeskTemplate` using which has 5 apps and each app has 1 window.
   AddEntry(base::GUID::GenerateRandomV4(), "template_1", base::Time::Now(),
+           DeskTemplateSource::kUser,
            CreateRestoreData(std::vector<int>(5, 1)));
 
   OpenOverviewAndShowTemplatesGrid();
@@ -1086,7 +1093,7 @@
   restore_data->ModifyWindowInfo(kAppId2, kWindowId2, window_info_2);
 
   AddEntry(base::GUID::GenerateRandomV4(), "template_1", base::Time::Now(),
-           std::move(restore_data));
+           DeskTemplateSource::kUser, std::move(restore_data));
 
   OpenOverviewAndShowTemplatesGrid();
 
@@ -1116,7 +1123,7 @@
   std::vector<int> window_info(
       kNumOverflowApps + DesksTemplatesIconContainer::kMaxIcons, 1);
   AddEntry(base::GUID::GenerateRandomV4(), "template_1", base::Time::Now(),
-           CreateRestoreData(window_info));
+           DeskTemplateSource::kUser, CreateRestoreData(window_info));
 
   OpenOverviewAndShowTemplatesGrid();
 
@@ -1153,7 +1160,7 @@
   std::vector<int> window_info(
       kNumOverflowApps + DesksTemplatesIconContainer::kMaxIcons, 2);
   AddEntry(base::GUID::GenerateRandomV4(), "template_1", base::Time::Now(),
-           CreateRestoreData(window_info));
+           DeskTemplateSource::kUser, CreateRestoreData(window_info));
 
   OpenOverviewAndShowTemplatesGrid();
 
@@ -1204,18 +1211,18 @@
 }
 
 // Tests that apps with multiple window are counted correctly.
-//   _______________________________________________________________________________
-//   |  _________  _________   _________________   _________________   _________
-//   | |  |       |  |       |   |       |       |   |       |       |   | |   |
-//   |  |   I   |  |   I   |   |   I      + 1  |   |   I   |  + 1  |   |  + 3  |
-//   | |  |_______|  |_______|   |_______|_______|   |_______|_______| |_______|
-//   |
-//   |_____________________________________________________________________________|
+//  _________________________________________________________________________
+//  |  ________   ________   ________________   ________________   ________ |
+//  | |       |  |       |  |       |       |  |       |       |  |       | |
+//  | |   I   |  |   I   |  |   I      + 1  |  |   I   |  + 1  |  |  + 3  | |
+//  | |_______|  |_______|  |_______|_______|  |_______|_______|  |_______| |
+//  |_______________________________________________________________________|
 //
 TEST_F(DesksTemplatesTest, IconViewMultipleWindows) {
   // Create a `DeskTemplate` that contains some apps with multiple windows and
   // more than kMaxIcons windows. The grid should appear like the above diagram.
   AddEntry(base::GUID::GenerateRandomV4(), "template_1", base::Time::Now(),
+           DeskTemplateSource::kUser,
            CreateRestoreData(std::vector<int>{1, 1, 2, 2, 3}));
 
   // Enter overview and show the Desks Templates Grid.
@@ -1262,7 +1269,7 @@
 TEST_F(DesksTemplatesTest, IconViewMoreThan99Windows) {
   // Create a `DeskTemplate` using which has 1 app with 101 windows.
   AddEntry(base::GUID::GenerateRandomV4(), "template_1", base::Time::Now(),
-           CreateRestoreData(std::vector<int>{101}));
+           DeskTemplateSource::kUser, CreateRestoreData(std::vector<int>{101}));
 
   // Enter overview and show the Desks Templates Grid.
   OpenOverviewAndShowTemplatesGrid();
@@ -1294,7 +1301,7 @@
   // `DesksTemplatesIconContainer::kMaxIcons` apps and each app has 1 window.
   std::vector<int> window_info(DesksTemplatesIconContainer::kMaxIcons, 1);
   AddEntry(base::GUID::GenerateRandomV4(), "template_1", base::Time::Now(),
-           CreateRestoreData(window_info));
+           DeskTemplateSource::kUser, CreateRestoreData(window_info));
 
   OpenOverviewAndShowTemplatesGrid();
 
@@ -1318,7 +1325,7 @@
   // of those app ids to be unavailable.
   std::vector<int> window_info(4, 1);
   AddEntry(base::GUID::GenerateRandomV4(), "template", base::Time::Now(),
-           CreateRestoreData(window_info));
+           DeskTemplateSource::kUser, CreateRestoreData(window_info));
 
   // `CreateRestoreData` creates the windows with app ids of "0", "1", "2", etc.
   // Set 2 of those app ids to be unavailable.
@@ -1351,7 +1358,7 @@
   // of those app ids to be unavailable.
   std::vector<int> window_info(8, 1);
   AddEntry(base::GUID::GenerateRandomV4(), "template", base::Time::Now(),
-           CreateRestoreData(window_info));
+           DeskTemplateSource::kUser, CreateRestoreData(window_info));
 
   // `CreateRestoreData` creates the windows with app ids of "0", "1", "2", etc.
   // Set 2 of those app ids to be unavailable.
@@ -1383,7 +1390,7 @@
   // Create a `DeskTemplate` which has 10 apps and each app has 1 window.
   std::vector<int> window_info(10, 1);
   AddEntry(base::GUID::GenerateRandomV4(), "template", base::Time::Now(),
-           CreateRestoreData(window_info));
+           DeskTemplateSource::kUser, CreateRestoreData(window_info));
 
   // Set all 10 app ids to be unavailable.
   auto* delegate = static_cast<TestDesksTemplatesDelegate*>(
@@ -3244,4 +3251,32 @@
       gfx::RectF(save_desk_as_template_widget->GetWindowBoundsInScreen())));
 }
 
+TEST_F(DesksTemplatesTest, AdminTemplate) {
+  AddEntry(base::GUID::GenerateRandomV4(), "template", base::Time::Now(),
+           DeskTemplateSource::kPolicy,
+           std::make_unique<app_restore::RestoreData>());
+
+  OpenOverviewAndShowTemplatesGrid();
+
+  // Tests that the name is read only and not focusable.
+  DesksTemplatesItemView* item_view =
+      GetItemViewFromTemplatesGrid(/*grid_item_index=*/0);
+  DesksTemplatesNameView* name_view = item_view->name_view();
+  EXPECT_TRUE(name_view->GetReadOnly());
+  EXPECT_FALSE(name_view->IsFocusable());
+
+  // Tests that there is an admin message in the time view and that the delete
+  // button is not created.
+  DesksTemplatesItemViewTestApi test_api(item_view);
+  EXPECT_EQ(u"Shared by your administrator", test_api.time_view()->GetText());
+  EXPECT_FALSE(test_api.delete_button());
+
+  // Tests that the name view cannot be tabbed into for admin templates, as they
+  // aren't editable anyhow.
+  SendKey(ui::VKEY_TAB);
+  EXPECT_EQ(item_view, GetHighlightedView());
+  SendKey(ui::VKEY_TAB);
+  EXPECT_NE(name_view, GetHighlightedView());
+}
+
 }  // namespace ash
diff --git a/ash/wm/overview/overview_grid.cc b/ash/wm/overview/overview_grid.cc
index f6c9148..02e576d 100644
--- a/ash/wm/overview/overview_grid.cc
+++ b/ash/wm/overview/overview_grid.cc
@@ -284,7 +284,7 @@
 
   // Show plus icon if drag a tab from a multi-tab window.
   widget->SetContentsView(std::make_unique<DropTargetView>(
-      dragged_window->GetProperty(ash::kTabDraggingSourceWindowKey)));
+      dragged_window->GetProperty(kTabDraggingSourceWindowKey)));
   aura::Window* drop_target_window = widget->GetNativeWindow();
   drop_target_window->parent()->StackChildAtBottom(drop_target_window);
   widget->Show();
@@ -293,7 +293,7 @@
 
 // Creates `save_desk_as_template_widget_`. It contains a button that saves the
 // active desk as a template.
-std::unique_ptr<views::Widget> SaveDeskAsTemplateWidget(
+std::unique_ptr<views::Widget> CreateSaveDeskAsTemplateWidget(
     aura::Window* root_window) {
   views::Widget::InitParams params;
   params.type = views::Widget::InitParams::TYPE_POPUP;
@@ -1898,7 +1898,8 @@
   }
 
   if (!save_desk_as_template_widget_) {
-    save_desk_as_template_widget_ = SaveDeskAsTemplateWidget(root_window_);
+    save_desk_as_template_widget_ =
+        CreateSaveDeskAsTemplateWidget(root_window_);
     save_desk_as_template_widget_->SetContentsView(
         std::make_unique<SaveDeskTemplateButton>(base::BindRepeating(
             &OverviewGrid::OnSaveDeskAsTemplateButtonPressed,
@@ -2456,7 +2457,7 @@
            ->disable_app_id_check_for_desk_templates() ||
        !full_restore::GetAppId(window).empty());
   int addend = increment ? 1 : -1;
-  if (!ash::DeskTemplate::IsAppTypeSupported(window) || !has_restore_id)
+  if (!DeskTemplate::IsAppTypeSupported(window) || !has_restore_id)
     num_unsupported_windows_ += addend;
   else if (Shell::Get()->desks_templates_delegate()->IsIncognitoWindow(window))
     num_incognito_windows_ += addend;
diff --git a/ash/wm/overview/overview_highlight_controller.cc b/ash/wm/overview/overview_highlight_controller.cc
index 9c8670c..24c1b71 100644
--- a/ash/wm/overview/overview_highlight_controller.cc
+++ b/ash/wm/overview/overview_highlight_controller.cc
@@ -225,7 +225,11 @@
       for (DesksTemplatesItemView* template_item :
            templates_grid_view->grid_items()) {
         traversable_views.push_back(template_item);
-        traversable_views.push_back(template_item->name_view());
+
+        // Admin templates names cannot be edited or focused.
+        DesksTemplatesNameView* name_view = template_item->name_view();
+        if (name_view->IsFocusable())
+          traversable_views.push_back(template_item->name_view());
       }
     } else {
       for (auto& item : grid->window_list())
diff --git a/base/BUILD.gn b/base/BUILD.gn
index 46d1d5c..0ec830b0 100644
--- a/base/BUILD.gn
+++ b/base/BUILD.gn
@@ -1334,8 +1334,6 @@
       "debug/proc_maps_linux.cc",
       "debug/proc_maps_linux.h",
       "files/dir_reader_linux.h",
-      "files/file_path_watcher_linux.cc",
-      "files/file_path_watcher_linux.h",
       "files/file_util_linux.cc",
       "files/scoped_file_linux.cc",
       "process/internal_linux.cc",
@@ -1349,6 +1347,13 @@
     ]
   }
 
+  if (is_linux || is_chromeos || is_android || is_fuchsia) {
+    sources += [
+      "files/file_path_watcher_inotify.cc",
+      "files/file_path_watcher_inotify.h",
+    ]
+  }
+
   if (!is_nacl) {
     sources += [
       "base_paths.cc",
@@ -1683,8 +1688,6 @@
       "debug/elf_reader.h",
       "debug/proc_maps_linux.cc",
       "debug/proc_maps_linux.h",
-      "files/file_path_watcher_linux.cc",
-      "files/file_path_watcher_linux.h",
       "power_monitor/power_monitor_device_source_android.cc",
       "process/internal_linux.cc",
       "process/internal_linux.h",
@@ -1758,7 +1761,6 @@
       "files/file_descriptor_watcher_posix.cc",
       "files/file_descriptor_watcher_posix.h",
       "files/file_enumerator_posix.cc",
-      "files/file_path_watcher_fuchsia.cc",
       "files/file_posix.cc",
       "files/file_util_fuchsia.cc",
       "files/file_util_posix.cc",
@@ -3696,9 +3698,6 @@
       "task/thread_pool/task_tracker_posix_unittest.cc",
     ]
 
-    # TODO(crbug.com/851641): FilePatchWatcherImpl is not implemented.
-    sources -= [ "files/file_path_watcher_unittest.cc" ]
-
     deps += [
       ":test_interface_impl",
       ":test_log_listener_safe",
diff --git a/base/files/file_path_watcher_linux.cc b/base/files/file_path_watcher_inotify.cc
similarity index 95%
rename from base/files/file_path_watcher_linux.cc
rename to base/files/file_path_watcher_inotify.cc
index 9297ece..5a6b6f40 100644
--- a/base/files/file_path_watcher_linux.cc
+++ b/base/files/file_path_watcher_inotify.cc
@@ -27,7 +27,7 @@
 #include "base/containers/contains.h"
 #include "base/files/file_enumerator.h"
 #include "base/files/file_path.h"
-#include "base/files/file_path_watcher_linux.h"
+#include "base/files/file_path_watcher_inotify.h"
 #include "base/files/file_util.h"
 #include "base/lazy_instance.h"
 #include "base/location.h"
@@ -48,6 +48,8 @@
 
 namespace {
 
+#if !BUILDFLAG(IS_FUCHSIA)
+
 // The /proc path to max_user_watches.
 constexpr char kInotifyMaxUserWatchesPath[] =
     "/proc/sys/fs/inotify/max_user_watches";
@@ -61,6 +63,8 @@
 // /proc/sys/fs/inotify/max_user_watches fails.
 constexpr size_t kDefaultInotifyMaxUserWatches = 8192u;
 
+#endif  // !BUILDFLAG(IS_FUCHSIA)
+
 class FilePathWatcherImpl;
 class InotifyReader;
 
@@ -70,6 +74,10 @@
 // Get the maximum number of inotify watches can be used by a FilePathWatcher
 // instance. This is based on /proc/sys/fs/inotify/max_user_watches entry.
 size_t GetMaxNumberOfInotifyWatches() {
+#if BUILDFLAG(IS_FUCHSIA)
+  // Fuchsia has no limit on the number of watches.
+  return std::numeric_limits<int>::max();
+#else
   static const size_t max = []() {
     size_t max_number_of_inotify_watches = 0u;
 
@@ -82,6 +90,7 @@
     return max_number_of_inotify_watches / kExpectedFilePathWatchers;
   }();
   return g_override_max_inotify_watches ? g_override_max_inotify_watches : max;
+#endif  // if BUILDFLAG(IS_FUCHSIA)
 }
 
 class InotifyReaderThreadDelegate final : public PlatformThread::Delegate {
@@ -208,8 +217,7 @@
   //   - Only if the target being watched is a symbolic link.
   struct WatchEntry {
     explicit WatchEntry(const FilePath::StringType& dirname)
-        : watch(InotifyReader::kInvalidWatch),
-          subdir(dirname) {}
+        : watch(InotifyReader::kInvalidWatch), subdir(dirname) {}
 
     InotifyReader::Watch watch;
     FilePath::StringType subdir;
@@ -334,8 +342,8 @@
   return PlatformThread::CreateNonJoinable(0, &thread_delegate_);
 }
 
-InotifyReader::Watch InotifyReader::AddWatch(
-    const FilePath& path, FilePathWatcherImpl* watcher) {
+InotifyReader::Watch InotifyReader::AddWatch(const FilePath& path,
+                                             FilePathWatcherImpl* watcher) {
   if (!valid_)
     return kInvalidWatch;
 
@@ -347,8 +355,7 @@
   ScopedBlockingCall scoped_blocking_call(FROM_HERE, BlockingType::WILL_BLOCK);
   Watch watch = inotify_add_watch(inotify_fd_, path.value().c_str(),
                                   IN_ATTRIB | IN_CREATE | IN_DELETE |
-                                  IN_CLOSE_WRITE | IN_MOVE |
-                                  IN_ONLYDIR);
+                                      IN_CLOSE_WRITE | IN_MOVE | IN_ONLYDIR);
 
   if (watch == kInvalidWatch)
     return kInvalidWatch;
@@ -440,10 +447,9 @@
       continue;
 
     // Check whether a path component of |target_| changed.
-    bool change_on_target_path =
-        child.empty() ||
-        (child == watch_entry.linkname) ||
-        (child == watch_entry.subdir);
+    bool change_on_target_path = child.empty() ||
+                                 (child == watch_entry.linkname) ||
+                                 (child == watch_entry.subdir);
 
     // Check if the change references |target_| or a direct child of |target_|.
     bool target_changed;
@@ -452,8 +458,8 @@
       // |target_| = "/path/to/foo", this is for "foo". Here, check either:
       // - the target has no symlink: it is the target and it changed.
       // - the target has a symlink, and it matches |child|.
-      target_changed = (watch_entry.linkname.empty() ||
-                        child == watch_entry.linkname);
+      target_changed =
+          (watch_entry.linkname.empty() || child == watch_entry.linkname);
     } else {
       // The fired watch is for a WatchEntry with a subdir. Thus for a given
       // |target_| = "/path/to/foo", this is for {"/", "/path", "/path/to"}.
@@ -492,8 +498,7 @@
     //    disappears in this case.
     //  - One of the parent directories appears. The event corresponding to
     //    the target appearing might have been missed in this case, so recheck.
-    if (target_changed ||
-        (change_on_target_path && deleted) ||
+    if (target_changed || (change_on_target_path && deleted) ||
         (change_on_target_path && created && PathExists(target_))) {
       if (!did_update) {
         if (!UpdateRecursiveWatches(fired_watch, is_dir)) {
@@ -700,11 +705,9 @@
   // rather than followed. Following symlinks can easily lead to the undesirable
   // situation where the entire file system is being watched.
   FileEnumerator enumerator(
-      path,
-      true /* recursive enumeration */,
+      path, true /* recursive enumeration */,
       FileEnumerator::DIRECTORIES | FileEnumerator::SHOW_SYM_LINKS);
-  for (FilePath current = enumerator.Next();
-       !current.empty();
+  for (FilePath current = enumerator.Next(); !current.empty();
        current = enumerator.Next()) {
     DCHECK(enumerator.GetInfo().IsDirectory());
 
@@ -762,6 +765,10 @@
 
 bool FilePathWatcherImpl::AddWatchForBrokenSymlink(const FilePath& path,
                                                    WatchEntry* watch_entry) {
+#if BUILDFLAG(IS_FUCHSIA)
+  // Fuchsia does not support symbolic links.
+  return false;
+#else   // BUILDFLAG(IS_FUCHSIA)
   DCHECK_EQ(InotifyReader::kInvalidWatch, watch_entry->watch);
   FilePath link;
   if (!ReadSymbolicLink(path, &link))
@@ -782,12 +789,13 @@
     // TODO(craig) Symlinks only work if the parent directory for the target
     // exist. Ideally we should make sure we've watched all the components of
     // the symlink path for changes. See crbug.com/91561 for details.
-    DPLOG(WARNING) << "Watch failed for "  << link.DirName().value();
+    DPLOG(WARNING) << "Watch failed for " << link.DirName().value();
     return true;
   }
   watch_entry->watch = watch;
   watch_entry->linkname = link.BaseName().value();
   return true;
+#endif  // BUILDFLAG(IS_FUCHSIA)
 }
 
 bool FilePathWatcherImpl::HasValidWatchVector() const {
diff --git a/base/files/file_path_watcher_linux.h b/base/files/file_path_watcher_inotify.h
similarity index 77%
rename from base/files/file_path_watcher_linux.h
rename to base/files/file_path_watcher_inotify.h
index 0b27676..cc49ae8 100644
--- a/base/files/file_path_watcher_linux.h
+++ b/base/files/file_path_watcher_inotify.h
@@ -2,8 +2,8 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#ifndef BASE_FILES_FILE_PATH_WATCHER_LINUX_H_
-#define BASE_FILES_FILE_PATH_WATCHER_LINUX_H_
+#ifndef BASE_FILES_FILE_PATH_WATCHER_INOTIFY_H_
+#define BASE_FILES_FILE_PATH_WATCHER_INOTIFY_H_
 
 #include <stddef.h>
 
@@ -20,4 +20,4 @@
 
 }  // namespace base
 
-#endif  // BASE_FILES_FILE_PATH_WATCHER_LINUX_H_
+#endif  // BASE_FILES_FILE_PATH_WATCHER_INOTIFY_H_
diff --git a/base/files/file_path_watcher_unittest.cc b/base/files/file_path_watcher_unittest.cc
index 58d16505..f4487c8 100644
--- a/base/files/file_path_watcher_unittest.cc
+++ b/base/files/file_path_watcher_unittest.cc
@@ -44,8 +44,8 @@
 #include "base/files/file_descriptor_watcher_posix.h"
 #endif  // BUILDFLAG(IS_POSIX)
 
-#if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS)
-#include "base/files/file_path_watcher_linux.h"
+#if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS) || BUILDFLAG(IS_FUCHSIA)
+#include "base/files/file_path_watcher_inotify.h"
 #include "base/format_macros.h"
 #endif
 
@@ -156,7 +156,7 @@
 class FilePathWatcherTest : public testing::Test {
  public:
   FilePathWatcherTest()
-#if BUILDFLAG(IS_POSIX)
+#if BUILDFLAG(IS_POSIX) || BUILDFLAG(IS_FUCHSIA)
       : task_environment_(test::TaskEnvironment::MainThreadType::IO)
 #endif
   {
@@ -255,7 +255,13 @@
 }
 
 // Verify that moving the file into place is caught.
-TEST_F(FilePathWatcherTest, MovedFile) {
+#if BUILDFLAG(IS_FUCHSIA)
+// TODO(crbug.com/851641): Re-enable for Fuchsia when inotify is fixed.
+#define MAYBE_MovedFile DISABLED_MovedFile
+#else
+#define MAYBE_MovedFile MovedFile
+#endif
+TEST_F(FilePathWatcherTest, MAYBE_MovedFile) {
   FilePath source_file(temp_dir_.GetPath().AppendASCII("source"));
   ASSERT_TRUE(WriteFile(source_file, "content"));
 
@@ -269,7 +275,13 @@
   ASSERT_TRUE(WaitForEvents());
 }
 
-TEST_F(FilePathWatcherTest, DeletedFile) {
+#if BUILDFLAG(IS_FUCHSIA)
+// TODO(crbug.com/851641): Re-enable for Fuchsia when inotify is fixed.
+#define MAYBE_DeletedFile DISABLED_DeletedFile
+#else
+#define MAYBE_DeletedFile DeletedFile
+#endif
+TEST_F(FilePathWatcherTest, MAYBE_DeletedFile) {
   ASSERT_TRUE(WriteFile(test_file(), "content"));
 
   FilePathWatcher watcher;
@@ -330,7 +342,13 @@
   ASSERT_TRUE(WriteFile(test_file(), "content"));
 }
 
-TEST_F(FilePathWatcherTest, MultipleWatchersSingleFile) {
+#if BUILDFLAG(IS_FUCHSIA)
+// TODO(crbug.com/851641): Re-enable for Fuchsia when inotify is fixed.
+#define MAYBE_MultipleWatchersSingleFile DISABLED_MultipleWatchersSingleFile
+#else
+#define MAYBE_MultipleWatchersSingleFile MultipleWatchersSingleFile
+#endif
+TEST_F(FilePathWatcherTest, MAYBE_MultipleWatchersSingleFile) {
   FilePathWatcher watcher1, watcher2;
   std::unique_ptr<TestDelegate> delegate1(new TestDelegate(collector()));
   std::unique_ptr<TestDelegate> delegate2(new TestDelegate(collector()));
@@ -345,7 +363,13 @@
 
 // Verify that watching a file whose parent directory doesn't exist yet works if
 // the directory and file are created eventually.
-TEST_F(FilePathWatcherTest, NonExistentDirectory) {
+#if BUILDFLAG(IS_FUCHSIA)
+// TODO(crbug.com/851641): Re-enable for Fuchsia when inotify is fixed.
+#define MAYBE_NonExistentDirectory DISABLED_NonExistentDirectory
+#else
+#define MAYBE_NonExistentDirectory NonExistentDirectory
+#endif
+TEST_F(FilePathWatcherTest, MAYBE_NonExistentDirectory) {
   FilePathWatcher watcher;
   FilePath dir(temp_dir_.GetPath().AppendASCII("dir"));
   FilePath file(dir.AppendASCII("file"));
@@ -371,7 +395,13 @@
 
 // Exercises watch reconfiguration for the case that directories on the path
 // are rapidly created.
-TEST_F(FilePathWatcherTest, DirectoryChain) {
+#if BUILDFLAG(IS_FUCHSIA)
+// TODO(crbug.com/851641): Re-enable for Fuchsia when inotify is fixed.
+#define MAYBE_DirectoryChain DISABLED_DirectoryChain
+#else
+#define MAYBE_DirectoryChain DirectoryChain
+#endif
+TEST_F(FilePathWatcherTest, MAYBE_DirectoryChain) {
   FilePath path(temp_dir_.GetPath());
   std::vector<std::string> dir_names;
   for (int i = 0; i < 20; i++) {
@@ -402,7 +432,13 @@
   ASSERT_TRUE(WaitForEvents());
 }
 
-TEST_F(FilePathWatcherTest, DisappearingDirectory) {
+#if BUILDFLAG(IS_FUCHSIA)
+// TODO(crbug.com/851641): Re-enable for Fuchsia when inotify is fixed.
+#define MAYBE_DisappearingDirectory DISABLED_DisappearingDirectory
+#else
+#define MAYBE_DisappearingDirectory DisappearingDirectory
+#endif
+TEST_F(FilePathWatcherTest, MAYBE_DisappearingDirectory) {
   FilePathWatcher watcher;
   FilePath dir(temp_dir_.GetPath().AppendASCII("dir"));
   FilePath file(dir.AppendASCII("file"));
@@ -417,7 +453,13 @@
 }
 
 // Tests that a file that is deleted and reappears is tracked correctly.
-TEST_F(FilePathWatcherTest, DeleteAndRecreate) {
+#if BUILDFLAG(IS_FUCHSIA)
+// TODO(crbug.com/851641): Re-enable for Fuchsia when inotify is fixed.
+#define MAYBE_DeleteAndRecreate DISABLED_DeleteAndRecreate
+#else
+#define MAYBE_DeleteAndRecreate DeleteAndRecreate
+#endif
+TEST_F(FilePathWatcherTest, MAYBE_DeleteAndRecreate) {
   ASSERT_TRUE(WriteFile(test_file(), "content"));
   FilePathWatcher watcher;
   std::unique_ptr<TestDelegate> delegate(new TestDelegate(collector()));
@@ -433,7 +475,13 @@
   ASSERT_TRUE(WaitForEvents());
 }
 
-TEST_F(FilePathWatcherTest, WatchDirectory) {
+#if BUILDFLAG(IS_FUCHSIA)
+// TODO(crbug.com/851641): Re-enable for Fuchsia when inotify is fixed.
+#define MAYBE_WatchDirectory DISABLED_WatchDirectory
+#else
+#define MAYBE_WatchDirectory WatchDirectory
+#endif
+TEST_F(FilePathWatcherTest, MAYBE_WatchDirectory) {
   FilePathWatcher watcher;
   FilePath dir(temp_dir_.GetPath().AppendASCII("dir"));
   FilePath file1(dir.AppendASCII("file1"));
@@ -466,7 +514,13 @@
   ASSERT_TRUE(WaitForEvents());
 }
 
-TEST_F(FilePathWatcherTest, MoveParent) {
+#if BUILDFLAG(IS_FUCHSIA)
+// TODO(crbug.com/851641): Re-enable for Fuchsia when inotify is fixed.
+#define MAYBE_MoveParent DISABLED_MoveParent
+#else
+#define MAYBE_MoveParent MoveParent
+#endif
+TEST_F(FilePathWatcherTest, MAYBE_MoveParent) {
   FilePathWatcher file_watcher;
   FilePathWatcher subdir_watcher;
   FilePath dir(temp_dir_.GetPath().AppendASCII("dir"));
@@ -492,7 +546,13 @@
   ASSERT_TRUE(WaitForEvents());
 }
 
-TEST_F(FilePathWatcherTest, RecursiveWatch) {
+#if BUILDFLAG(IS_FUCHSIA)
+// TODO(crbug.com/851641): Re-enable for Fuchsia when inotify is fixed.
+#define MAYBE_RecursiveWatch DISABLED_RecursiveWatch
+#else
+#define MAYBE_RecursiveWatch RecursiveWatch
+#endif
+TEST_F(FilePathWatcherTest, MAYBE_RecursiveWatch) {
   FilePathWatcher watcher;
   FilePath dir(temp_dir_.GetPath().AppendASCII("dir"));
   std::unique_ptr<TestDelegate> delegate(new TestDelegate(collector()));
@@ -615,7 +675,13 @@
 }
 #endif  // BUILDFLAG(IS_POSIX) && !BUILDFLAG(IS_ANDROID)
 
-TEST_F(FilePathWatcherTest, MoveChild) {
+#if BUILDFLAG(IS_FUCHSIA)
+// TODO(crbug.com/851641): Re-enable for Fuchsia when inotify is fixed.
+#define MAYBE_MoveChild DISABLED_MoveChild
+#else
+#define MAYBE_MoveChild MoveChild
+#endif
+TEST_F(FilePathWatcherTest, MAYBE_MoveChild) {
   FilePathWatcher file_watcher;
   FilePathWatcher subdir_watcher;
   FilePath source_dir(temp_dir_.GetPath().AppendASCII("source"));
@@ -642,15 +708,19 @@
 }
 
 // Verify that changing attributes on a file is caught
-#if BUILDFLAG(IS_ANDROID)
+#if BUILDFLAG(IS_ANDROID) || BUILDFLAG(IS_FUCHSIA)
+// TODO(crbug.com/851641): Re-enable for Fuchsia when inotify is fixed.
+
 // Apps cannot change file attributes on Android in /sdcard as /sdcard uses the
 // "fuse" file system, while /data uses "ext4".  Running these tests in /data
 // would be preferable and allow testing file attributes and symlinks.
 // TODO(pauljensen): Re-enable when crbug.com/475568 is fixed and SetUp() places
 // the |temp_dir_| in /data.
-#define FileAttributesChanged DISABLED_FileAttributesChanged
+#define MAYBE_FileAttributesChanged DISABLED_FileAttributesChanged
+#else
+#define MAYBE_FileAttributesChanged FileAttributesChanged
 #endif  // BUILDFLAG(IS_ANDROID)
-TEST_F(FilePathWatcherTest, FileAttributesChanged) {
+TEST_F(FilePathWatcherTest, MAYBE_FileAttributesChanged) {
   ASSERT_TRUE(WriteFile(test_file(), "content"));
   FilePathWatcher watcher;
   std::unique_ptr<TestDelegate> delegate(new TestDelegate(collector()));
diff --git a/base/win/win_util.cc b/base/win/win_util.cc
index 7d78505a..2da6cbc 100644
--- a/base/win/win_util.cc
+++ b/base/win/win_util.cc
@@ -58,6 +58,7 @@
 #include "base/win/scoped_hstring.h"
 #include "base/win/scoped_propvariant.h"
 #include "base/win/shlwapi.h"
+#include "base/win/static_constants.h"
 #include "base/win/windows_version.h"
 #include "third_party/abseil-cpp/absl/types/optional.h"
 
@@ -864,6 +865,32 @@
   return current_session_id != glass_session_id;
 }
 
+#if !defined(OFFICIAL_BUILD)
+bool IsAppVerifierEnabled(const std::wstring& process_name) {
+  RegKey key;
+
+  // Look for GlobalFlag in the IFEO\chrome.exe key. If it is present then
+  // Application Verifier or gflags.exe are configured. Most GlobalFlag
+  // settings are experimentally determined to be incompatible with renderer
+  // code integrity and a safe set is not known so any GlobalFlag entry is
+  // assumed to mean that Application Verifier (or pageheap) are enabled.
+  // The settings are propagated to both 64-bit WOW6432Node versions of the
+  // registry on 64-bit Windows, so only one check is needed.
+  return key.Open(
+             HKEY_LOCAL_MACHINE,
+             (L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Image File "
+              L"Execution Options\\" +
+              process_name)
+                 .c_str(),
+             KEY_READ | KEY_WOW64_64KEY) == ERROR_SUCCESS &&
+         key.HasValue(L"GlobalFlag");
+}
+#endif  // !defined(OFFICIAL_BUILD)
+
+bool IsAppVerifierLoaded() {
+  return GetModuleHandleA(kApplicationVerifierDllName);
+}
+
 ScopedDomainStateForTesting::ScopedDomainStateForTesting(bool state)
     : initial_state_(IsEnrolledToDomain()) {
   *GetDomainEnrollmentStateStorage() = state;
diff --git a/base/win/win_util.h b/base/win/win_util.h
index 242b7e3..abcb246 100644
--- a/base/win/win_util.h
+++ b/base/win/win_util.h
@@ -232,6 +232,17 @@
 // Returns true if current session is a remote session.
 BASE_EXPORT bool IsCurrentSessionRemote();
 
+#if !defined(OFFICIAL_BUILD)
+// IsAppVerifierEnabled() indicates whether a newly created process will get
+// Application Verifier or pageheap injected into it. Only available in
+// unofficial builds to prevent abuse.
+BASE_EXPORT bool IsAppVerifierEnabled(const std::wstring& process_name);
+#endif  // !defined(OFFICIAL_BUILD)
+
+// IsAppVerifierLoaded() indicates whether Application Verifier is *already*
+// loaded into the current process.
+BASE_EXPORT bool IsAppVerifierLoaded();
+
 // Allows changing the domain enrolled state for the life time of the object.
 // The original state is restored upon destruction.
 class BASE_EXPORT ScopedDomainStateForTesting {
diff --git a/build/OWNERS.setnoparent b/build/OWNERS.setnoparent
index ec033cf..f0cd791 100644
--- a/build/OWNERS.setnoparent
+++ b/build/OWNERS.setnoparent
@@ -69,3 +69,7 @@
 # that can make infra changes.
 file://infra/config/groups/cq-usage/CQ_USAGE_OWNERS
 file://infra/config/groups/sheriff-rotations/CHROMIUM_OWNERS
+
+# Origin Trials owners are responsible for determining trials that need to be
+# completed manually.
+file://third_party/blink/common/origin_trials/OT_OWNERS
diff --git a/build/fuchsia/linux.sdk.sha1 b/build/fuchsia/linux.sdk.sha1
index d36ddce..22221e6 100644
--- a/build/fuchsia/linux.sdk.sha1
+++ b/build/fuchsia/linux.sdk.sha1
@@ -1 +1 @@
-7.20220325.2.1
+7.20220325.3.1
diff --git a/build/fuchsia/linux_internal.sdk.sha1 b/build/fuchsia/linux_internal.sdk.sha1
index d36ddce..22221e6 100644
--- a/build/fuchsia/linux_internal.sdk.sha1
+++ b/build/fuchsia/linux_internal.sdk.sha1
@@ -1 +1 @@
-7.20220325.2.1
+7.20220325.3.1
diff --git a/build/fuchsia/mac.sdk.sha1 b/build/fuchsia/mac.sdk.sha1
index d36ddce..22221e6 100644
--- a/build/fuchsia/mac.sdk.sha1
+++ b/build/fuchsia/mac.sdk.sha1
@@ -1 +1 @@
-7.20220325.2.1
+7.20220325.3.1
diff --git a/build/util/BUILD.gn b/build/util/BUILD.gn
index 2745449..834cc1ed 100644
--- a/build/util/BUILD.gn
+++ b/build/util/BUILD.gn
@@ -30,7 +30,6 @@
 
 group("test_results") {
   data = [
-    "//.vpython",
     "//.vpython3",
     "//build/util/lib/__init__.py",
     "//build/util/lib/results/",
diff --git a/build/util/generate_wrapper.gni b/build/util/generate_wrapper.gni
index d63720b..316d6b9 100644
--- a/build/util/generate_wrapper.gni
+++ b/build/util/generate_wrapper.gni
@@ -20,8 +20,6 @@
 #     build product. Paths can be relative to the containing gn file
 #     or source-absolute.
 #   executable_args: List of arguments to write into the wrapper.
-#   use_vpython3: If false, invoke the generated wrapper with vpython instead
-#     of vpython3.
 #
 # Example wrapping a checked-in script:
 #   generate_wrapper("sample_wrapper") {
@@ -70,7 +68,10 @@
     if (!defined(data)) {
       data = []
     }
-    data += [ _wrapper_script ]
+    data += [
+      _wrapper_script,
+      "//.vpython3",
+    ]
     outputs = [ _wrapper_script ]
 
     _rebased_executable_to_wrap =
@@ -92,10 +93,6 @@
       _script_language,
     ]
 
-    if (!defined(invoker.use_vpython3) || invoker.use_vpython3) {
-      args += [ "--use-vpython3" ]
-      data += [ "//.vpython3" ]
-    }
     args += [ "--" ]
     args += _wrapped_arguments
 
diff --git a/build/util/generate_wrapper.py b/build/util/generate_wrapper.py
index 07167e86..ce264ef 100755
--- a/build/util/generate_wrapper.py
+++ b/build/util/generate_wrapper.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env vpython
+#!/usr/bin/env python3
 # Copyright 2019 The Chromium Authors. All rights reserved.
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
@@ -15,7 +15,7 @@
 # The interpreter doesn't know about the script, so we have bash
 # inject the script location.
 BASH_TEMPLATE = textwrap.dedent("""\
-    #!/usr/bin/env {vpython}
+    #!/usr/bin/env vpython3
     _SCRIPT_LOCATION = __file__
     {script}
     """)
@@ -27,7 +27,7 @@
 # directly.
 BATCH_TEMPLATE = textwrap.dedent("""\
     @SETLOCAL ENABLEDELAYEDEXPANSION \
-      & {vpython}.bat -x "%~f0" %* \
+      & vpython3.bat -x "%~f0" %* \
       & EXIT /B !ERRORLEVEL!
     _SCRIPT_LOCATION = __file__
     {script}
@@ -172,8 +172,7 @@
         executable_path=str(args.executable),
         executable_args=str(args.executable_args))
     template = SCRIPT_TEMPLATES[args.script_language]
-    wrapper_script.write(
-        template.format(script=py_contents, vpython=args.vpython))
+    wrapper_script.write(template.format(script=py_contents))
   os.chmod(args.wrapper_script, 0o750)
 
   return 0
@@ -195,12 +194,6 @@
       '--script-language',
       choices=SCRIPT_TEMPLATES.keys(),
       help='Language in which the wrapper script will be written.')
-  parser.add_argument('--use-vpython3',
-                      dest='vpython',
-                      action='store_const',
-                      const='vpython3',
-                      default='vpython',
-                      help='Use vpython3 instead of vpython')
   parser.add_argument(
       'executable_args', nargs='*',
       help='Arguments to wrap into the executable.')
diff --git a/chrome/VERSION b/chrome/VERSION
index 2d0a593..c3d1215 100644
--- a/chrome/VERSION
+++ b/chrome/VERSION
@@ -1,4 +1,4 @@
 MAJOR=102
 MINOR=0
-BUILD=4965
+BUILD=4966
 PATCH=0
diff --git a/chrome/android/features/start_surface/internal/java/src/org/chromium/chrome/features/start_surface/StartSurfaceCoordinator.java b/chrome/android/features/start_surface/internal/java/src/org/chromium/chrome/features/start_surface/StartSurfaceCoordinator.java
index 0f25002..2f3fcb8 100644
--- a/chrome/android/features/start_surface/internal/java/src/org/chromium/chrome/features/start_surface/StartSurfaceCoordinator.java
+++ b/chrome/android/features/start_surface/internal/java/src/org/chromium/chrome/features/start_surface/StartSurfaceCoordinator.java
@@ -283,7 +283,8 @@
                 mIsStartSurfaceEnabled ? this::initializeSecondaryTasksSurface : null,
                 mIsStartSurfaceEnabled, mActivity, mBrowserControlsManager,
                 this::isActivityFinishingOrDestroyed, excludeMVTiles, excludeQueryTiles,
-                startSurfaceOneshotSupplier, hadWarmStart, jankTracker);
+                startSurfaceOneshotSupplier, hadWarmStart, jankTracker,
+                mTasksSurface != null ? mTasksSurface::initializeMVTiles : null);
 
         // Show feed loading image.
         if (mStartSurfaceMediator.shouldShowFeedPlaceholder()) {
diff --git a/chrome/android/features/start_surface/internal/java/src/org/chromium/chrome/features/start_surface/StartSurfaceLayout.java b/chrome/android/features/start_surface/internal/java/src/org/chromium/chrome/features/start_surface/StartSurfaceLayout.java
index ca74f81..78a0d3eb 100644
--- a/chrome/android/features/start_surface/internal/java/src/org/chromium/chrome/features/start_surface/StartSurfaceLayout.java
+++ b/chrome/android/features/start_surface/internal/java/src/org/chromium/chrome/features/start_surface/StartSurfaceLayout.java
@@ -698,23 +698,10 @@
     }
 
     /**
-     * When state is SHOWN_HOMEPAGE or SHOWING_HOMEPAGE or SHOWING_START, state surface homepage is
-     * showing. When state is StartSurfaceState.SHOWING_PREVIOUS and the previous state is
-     * SHOWN_HOMEPAGE or NOT_SHOWN, homepage is showing.
      * @return Whether start surface homepage is showing.
      */
     private boolean isShowingStartSurfaceHomepage() {
-        @StartSurfaceState
-        int currentState = mController.getStartSurfaceState();
-        @StartSurfaceState
-        int previousState = mController.getPreviousStartSurfaceState();
-
-        return currentState == StartSurfaceState.SHOWN_HOMEPAGE
-                || currentState == StartSurfaceState.SHOWING_HOMEPAGE
-                || currentState == StartSurfaceState.SHOWING_START
-                || (currentState == StartSurfaceState.SHOWING_PREVIOUS
-                        && (previousState == StartSurfaceState.SHOWN_HOMEPAGE
-                                || previousState == StartSurfaceState.NOT_SHOWN));
+        return mController.isShowingStartSurfaceHomepage();
     }
 
     private boolean isHidingStartSurfaceHomepage() {
diff --git a/chrome/android/features/start_surface/internal/java/src/org/chromium/chrome/features/start_surface/StartSurfaceMediator.java b/chrome/android/features/start_surface/internal/java/src/org/chromium/chrome/features/start_surface/StartSurfaceMediator.java
index 459e41c..644b9a0 100644
--- a/chrome/android/features/start_surface/internal/java/src/org/chromium/chrome/features/start_surface/StartSurfaceMediator.java
+++ b/chrome/android/features/start_surface/internal/java/src/org/chromium/chrome/features/start_surface/StartSurfaceMediator.java
@@ -106,6 +106,7 @@
     private final ObserverList<StartSurface.StateObserver> mStateObservers = new ObserverList<>();
     private final boolean mHadWarmStart;
     private final boolean mExcludeQueryTiles;
+    private final Runnable mInitializeMVTilesRunnable;
 
     // Boolean histogram used to record whether cached
     // ChromePreferenceKeys.FEED_ARTICLES_LIST_VISIBLE is consistent with
@@ -174,7 +175,7 @@
             BrowserControlsStateProvider browserControlsStateProvider,
             ActivityStateChecker activityStateChecker, boolean excludeMVTiles,
             boolean excludeQueryTiles, OneshotSupplier<StartSurface> startSurfaceSupplier,
-            boolean hadWarmStart, JankTracker jankTracker) {
+            boolean hadWarmStart, JankTracker jankTracker, Runnable initializeMVTilesRunnable) {
         mController = controller;
         mTabSwitcherContainer = tabSwitcherContainer;
         mTabModelSelector = tabModelSelector;
@@ -190,6 +191,7 @@
         mHadWarmStart = hadWarmStart;
         mJankTracker = jankTracker;
         mLaunchOrigin = NewTabPageLaunchOrigin.UNKNOWN;
+        mInitializeMVTilesRunnable = initializeMVTilesRunnable;
 
         if (mPropertyModel != null) {
             assert mIsStartSurfaceEnabled;
@@ -345,11 +347,15 @@
             LensMetrics.recordShown(LensEntryPoint.TASKS_SURFACE, shouldShowLensButton);
             mPropertyModel.set(IS_LENS_BUTTON_VISIBLE, shouldShowLensButton);
 
+            // This is for Instant Start when overview is already visible while the omnibox, Feed
+            // and MV tiles haven't been set.
             if (mController.overviewVisible()) {
                 mOmniboxStub.addUrlFocusChangeListener(mUrlFocusChangeListener);
-                if (mStartSurfaceState == StartSurfaceState.SHOWN_HOMEPAGE
-                        && mExploreSurfaceCoordinatorFactory != null) {
-                    setExploreSurfaceVisibility(!mIsIncognito);
+                if (mStartSurfaceState == StartSurfaceState.SHOWN_HOMEPAGE) {
+                    if (mExploreSurfaceCoordinatorFactory != null) {
+                        setExploreSurfaceVisibility(!mIsIncognito);
+                    }
+                    if (mInitializeMVTilesRunnable != null) mInitializeMVTilesRunnable.run();
                 }
             }
         }
@@ -502,14 +508,14 @@
             setSecondaryTasksSurfaceVisibility(
                     /* isVisible= */ true, /* skipUpdateController = */ true);
         } else if (mStartSurfaceState == StartSurfaceState.SHOWN_HOMEPAGE) {
-            setExploreSurfaceVisibility(!mIsIncognito && mExploreSurfaceCoordinatorFactory != null);
             boolean hasNormalTab = getNormalTabCount() > 0;
 
             // If new home surface for home button is enabled, MV tiles and carousel tab switcher
             // will not show.
+            setMVTilesVisibility(!mIsIncognito && !mHideMVForNewSurface);
             setTabCarouselVisibility(
                     hasNormalTab && !mIsIncognito && !mHideTabCarouselForNewSurface);
-            setMVTilesVisibility(!mIsIncognito && !mHideMVForNewSurface);
+            setExploreSurfaceVisibility(!mIsIncognito && mExploreSurfaceCoordinatorFactory != null);
             // TODO(qinmin): show query tiles when flag is enabled.
             setQueryTilesVisibility(false);
             setFakeBoxVisibility(!mIsIncognito);
@@ -691,6 +697,19 @@
                 && mStartSurfaceState != StartSurfaceState.DISABLED;
     }
 
+    @Override
+    public boolean isShowingStartSurfaceHomepage() {
+        // When state is SHOWN_HOMEPAGE or SHOWING_HOMEPAGE or SHOWING_START, state surface homepage
+        // is showing. When state is StartSurfaceState.SHOWING_PREVIOUS and the previous state is
+        // SHOWN_HOMEPAGE or NOT_SHOWN, homepage is showing.
+        return mStartSurfaceState == StartSurfaceState.SHOWN_HOMEPAGE
+                || mStartSurfaceState == StartSurfaceState.SHOWING_HOMEPAGE
+                || mStartSurfaceState == StartSurfaceState.SHOWING_START
+                || (mStartSurfaceState == StartSurfaceState.SHOWING_PREVIOUS
+                        && (mPreviousStartSurfaceState == StartSurfaceState.SHOWN_HOMEPAGE
+                                || mPreviousStartSurfaceState == StartSurfaceState.NOT_SHOWN));
+    }
+
     // Implements TabSwitcher.OverviewModeObserver.
     @Override
     public void startedShowing() {
@@ -921,7 +940,8 @@
     }
 
     private void setMVTilesVisibility(boolean isVisible) {
-        if (mExcludeMVTiles || isVisible == mPropertyModel.get(MV_TILES_VISIBLE)) return;
+        if (mExcludeMVTiles) return;
+        if (isVisible && mInitializeMVTilesRunnable != null) mInitializeMVTilesRunnable.run();
         mPropertyModel.set(MV_TILES_VISIBLE, isVisible);
     }
 
diff --git a/chrome/android/features/start_surface/internal/javatests/src/org/chromium/chrome/features/start_surface/StartSurfaceMVTilesTest.java b/chrome/android/features/start_surface/internal/javatests/src/org/chromium/chrome/features/start_surface/StartSurfaceMVTilesTest.java
index 9fb9987a..3e9860d 100644
--- a/chrome/android/features/start_surface/internal/javatests/src/org/chromium/chrome/features/start_surface/StartSurfaceMVTilesTest.java
+++ b/chrome/android/features/start_surface/internal/javatests/src/org/chromium/chrome/features/start_surface/StartSurfaceMVTilesTest.java
@@ -215,6 +215,12 @@
 
         TabUiTestHelper.enterTabSwitcher(cta);
         TestThreadUtils.runOnUiThreadBlocking(() -> {
+            Assert.assertFalse(startSurfaceCoordinator.isMVTilesInitializedForTesting());
+        });
+
+        TestThreadUtils.runOnUiThreadBlocking(() -> cta.getTabCreator(false).launchNTP());
+        onViewWaiting(withId(org.chromium.chrome.start_surface.R.id.primary_tasks_surface_view));
+        TestThreadUtils.runOnUiThreadBlocking(() -> {
             Assert.assertTrue(startSurfaceCoordinator.isMVTilesInitializedForTesting());
         });
     }
diff --git a/chrome/android/features/start_surface/internal/javatests/src/org/chromium/chrome/features/start_surface/StartSurfaceTest.java b/chrome/android/features/start_surface/internal/javatests/src/org/chromium/chrome/features/start_surface/StartSurfaceTest.java
index be516a4..1f72bab 100644
--- a/chrome/android/features/start_surface/internal/javatests/src/org/chromium/chrome/features/start_surface/StartSurfaceTest.java
+++ b/chrome/android/features/start_surface/internal/javatests/src/org/chromium/chrome/features/start_surface/StartSurfaceTest.java
@@ -832,8 +832,8 @@
         }
 
         ChromeTabbedActivity cta = mActivityTestRule.getActivity();
-        CriteriaHelper.pollUiThread(
-                () -> cta.getLayoutManager() != null && cta.getLayoutManager().overviewVisible());
+        StartSurfaceTestUtils.waitForOverviewVisible(
+                mLayoutChangedCallbackHelper, mCurrentlyActiveLayout);
         StartSurfaceTestUtils.waitForTabModel(cta);
         TestThreadUtils.runOnUiThreadBlocking(
                 () -> { cta.getTabModelSelector().getModel(false).closeAllTabs(); });
diff --git a/chrome/android/features/start_surface/internal/junit/src/org/chromium/chrome/features/start_surface/StartSurfaceMediatorUnitTest.java b/chrome/android/features/start_surface/internal/junit/src/org/chromium/chrome/features/start_surface/StartSurfaceMediatorUnitTest.java
index aedea8f0..edb4c6d 100644
--- a/chrome/android/features/start_surface/internal/junit/src/org/chromium/chrome/features/start_surface/StartSurfaceMediatorUnitTest.java
+++ b/chrome/android/features/start_surface/internal/junit/src/org/chromium/chrome/features/start_surface/StartSurfaceMediatorUnitTest.java
@@ -142,6 +142,8 @@
     private PrefService mPrefService;
     @Mock
     private OneshotSupplier<StartSurface> mStartSurfaceSupplier;
+    @Mock
+    private Runnable mInitializeMVTilesRunnable;
     @Captor
     private ArgumentCaptor<TabModelSelectorObserver> mTabModelSelectorObserverCaptor;
     @Captor
@@ -1263,6 +1265,20 @@
         assertThat(mPropertyModel.get(IS_TAB_CAROUSEL_TITLE_VISIBLE), equalTo(false));
     }
 
+    @Test
+    public void testInitializeMVTilesWhenShownHomepage() {
+        doReturn(false).when(mTabModelSelector).isIncognitoSelected();
+        doReturn(mVoiceRecognitionHandler).when(mOmniboxStub).getVoiceRecognitionHandler();
+        doReturn(true).when(mVoiceRecognitionHandler).isVoiceSearchEnabled();
+        doReturn(2).when(mNormalTabModel).getCount();
+        doReturn(true).when(mTabModelSelector).isTabStateInitialized();
+
+        StartSurfaceMediator mediator =
+                createStartSurfaceMediator(/* isStartSurfaceEnabled= */ true, false);
+        mediator.setOverviewState(StartSurfaceState.SHOWN_HOMEPAGE);
+        verify(mInitializeMVTilesRunnable).run();
+    }
+
     private StartSurfaceMediator createStartSurfaceMediator(
             boolean isStartSurfaceEnabled, boolean excludeMVTiles) {
         return createStartSurfaceMediator(
@@ -1289,7 +1305,7 @@
                         isStartSurfaceEnabled, ContextUtils.getApplicationContext(),
                         mBrowserControlsStateProvider, mActivityStateChecker, excludeMVTiles,
                         true /* excludeQueryTiles */, mStartSurfaceSupplier, hadWarmStart,
-                        new DummyJankTracker());
+                        new DummyJankTracker(), mInitializeMVTilesRunnable);
         return mediator;
     }
 }
diff --git a/chrome/android/features/start_surface/public/java/src/org/chromium/chrome/features/start_surface/StartSurface.java b/chrome/android/features/start_surface/public/java/src/org/chromium/chrome/features/start_surface/StartSurface.java
index a5ca3c7..1bf2d9b 100644
--- a/chrome/android/features/start_surface/public/java/src/org/chromium/chrome/features/start_surface/StartSurface.java
+++ b/chrome/android/features/start_surface/public/java/src/org/chromium/chrome/features/start_surface/StartSurface.java
@@ -199,6 +199,11 @@
          * @return The Tab switcher container view.
          */
         ViewGroup getTabSwitcherContainer();
+
+        /*
+         * Returns whether start surface homepage is showing.
+         */
+        boolean isShowingStartSurfaceHomepage();
     }
 
     /**
diff --git a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/TasksSurface.java b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/TasksSurface.java
index 1440443..e1261a9a 100644
--- a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/TasksSurface.java
+++ b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/TasksSurface.java
@@ -33,6 +33,13 @@
     void initialize();
 
     /**
+     * Called to initialize MV tiles.
+     * It should be called before MV tiles is showing.
+     * It might be called many times.
+     */
+    void initializeMVTiles();
+
+    /**
      * Set the listener to get the {@link Layout#onTabSelecting} event from the Grid Tab Switcher.
      * @param listener The {@link TabSwitcher.OnTabSelectingListener} to use.
      */
diff --git a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/TasksSurfaceCoordinator.java b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/TasksSurfaceCoordinator.java
index a9936b4..7ba06c1 100644
--- a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/TasksSurfaceCoordinator.java
+++ b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/TasksSurfaceCoordinator.java
@@ -167,14 +167,30 @@
     @Override
     public void initialize() {
         assert LibraryLoader.getInstance().isInitialized();
-        if (!mIsMVTilesInitialized && mMostVisitedCoordinator != null) {
-            initializeMVTiles();
-            mIsMVTilesInitialized = true;
-        }
         mMediator.initialize();
     }
 
     @Override
+    public void initializeMVTiles() {
+        if (!LibraryLoader.getInstance().isInitialized() || mIsMVTilesInitialized
+                || mMostVisitedCoordinator == null) {
+            return;
+        }
+
+        Profile profile = Profile.getLastUsedRegularProfile();
+        MostVisitedTileNavigationDelegate navigationDelegate =
+                new MostVisitedTileNavigationDelegate(mActivity, profile, mParentTabSupplier);
+        mSuggestionsUiDelegate =
+                new MostVisitedSuggestionsUiDelegate(navigationDelegate, profile, mSnackbarManager);
+        mTileGroupDelegate =
+                new TileGroupDelegateImpl(mActivity, profile, navigationDelegate, mSnackbarManager);
+
+        mMostVisitedCoordinator.initWithNative(
+                mSuggestionsUiDelegate, mTileGroupDelegate, enabled -> {});
+        mIsMVTilesInitialized = true;
+    }
+
+    @Override
     public void setOnTabSelectingListener(TabSwitcher.OnTabSelectingListener listener) {
         if (mTabSwitcher != null) {
             mTabSwitcher.setOnTabSelectingListener(listener);
@@ -272,19 +288,6 @@
         return mIsMVTilesInitialized;
     }
 
-    private void initializeMVTiles() {
-        Profile profile = Profile.getLastUsedRegularProfile();
-        MostVisitedTileNavigationDelegate navigationDelegate =
-                new MostVisitedTileNavigationDelegate(mActivity, profile, mParentTabSupplier);
-        mSuggestionsUiDelegate =
-                new MostVisitedSuggestionsUiDelegate(navigationDelegate, profile, mSnackbarManager);
-        mTileGroupDelegate =
-                new TileGroupDelegateImpl(mActivity, profile, navigationDelegate, mSnackbarManager);
-
-        mMostVisitedCoordinator.initWithNative(
-                mSuggestionsUiDelegate, mTileGroupDelegate, enabled -> {});
-    }
-
     /** Suggestions UI Delegate for constructing the TileGroup. */
     private class MostVisitedSuggestionsUiDelegate extends SuggestionsUiDelegateImpl {
         public MostVisitedSuggestionsUiDelegate(SuggestionsNavigationDelegate navigationDelegate,
diff --git a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/PriceTrackingUtilities.java b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/PriceTrackingUtilities.java
index 688033c..dc0d089f 100644
--- a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/PriceTrackingUtilities.java
+++ b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/PriceTrackingUtilities.java
@@ -98,9 +98,8 @@
         return getPriceTrackingEnabled() || getPriceTrackingNotificationsEnabled();
     }
 
-    /**
-     * Update SharedPreferences when users turn on/off the feature tracking prices on tabs.
-     */
+    // TODO(crbug.com/1307949): Clean up this api.
+    @Deprecated
     public static void flipTrackPricesOnTabs() {
         final boolean enableTrackPricesOnTabs = SHARED_PREFERENCES_MANAGER.readBoolean(
                 TRACK_PRICES_ON_TABS, isPriceTrackingEnabled());
@@ -108,6 +107,13 @@
     }
 
     /**
+     * Update SharedPreferences when users turn on/off the feature tracking prices on tabs.
+     */
+    public static void setTrackPricesOnTabsEnabled(boolean enabled) {
+        SHARED_PREFERENCES_MANAGER.writeBoolean(TRACK_PRICES_ON_TABS, enabled);
+    }
+
+    /**
      * @return Whether the track prices on tabs is turned on by users.
      */
     public static boolean isTrackPricesOnTabsEnabled() {
@@ -256,20 +262,19 @@
      */
     public static boolean allowUsersToDisablePriceAnnotations() {
         if (FeatureList.isInitialized()) {
-            return isPriceTrackingEnabled()
+            return isPriceTrackingEligible()
                     && ChromeFeatureList.getFieldTrialParamByFeatureAsBoolean(
                             ChromeFeatureList.COMMERCE_PRICE_TRACKING,
                             ALLOW_DISABLE_PRICE_ANNOTATIONS_PARAM, true);
         }
-        return isPriceTrackingEnabled();
+        return isPriceTrackingEligible();
     }
 
+    // TODO(crbug.com/1307949): Clean up price tracking menu.
     /**
      * @return whether we should show the PriceTrackingSettings menu item in grid tab switcher.
      */
     public static boolean shouldShowPriceTrackingMenu() {
-        return isPriceTrackingEligible()
-                && (allowUsersToDisablePriceAnnotations()
-                        || getPriceTrackingNotificationsEnabled());
+        return false;
     }
 }
diff --git a/chrome/android/features/tab_ui/javatests/src/org/chromium/chrome/browser/tasks/tab_management/PriceTrackingDialogTest.java b/chrome/android/features/tab_ui/javatests/src/org/chromium/chrome/browser/tasks/tab_management/PriceTrackingDialogTest.java
index ecc6504..9273376 100644
--- a/chrome/android/features/tab_ui/javatests/src/org/chromium/chrome/browser/tasks/tab_management/PriceTrackingDialogTest.java
+++ b/chrome/android/features/tab_ui/javatests/src/org/chromium/chrome/browser/tasks/tab_management/PriceTrackingDialogTest.java
@@ -45,6 +45,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.Feature;
 import org.chromium.base.test.util.FlakyTest;
 import org.chromium.base.test.util.Restriction;
@@ -73,6 +74,7 @@
         "force-fieldtrials=Study/Group"})
 @Restriction({UiRestriction.RESTRICTION_TYPE_PHONE, RESTRICTION_TYPE_NON_LOW_END_DEVICE})
 @Features.DisableFeatures({ChromeFeatureList.START_SURFACE_ANDROID})
+@DisabledTest(message = "crbug.com/1307949")
 public class PriceTrackingDialogTest {
     // clang-format on
     private static final String BASE_PARAMS =
diff --git a/chrome/android/features/tab_ui/javatests/src/org/chromium/chrome/browser/tasks/tab_management/TabListViewHolderTest.java b/chrome/android/features/tab_ui/javatests/src/org/chromium/chrome/browser/tasks/tab_management/TabListViewHolderTest.java
index c257dbdd..6dfd26ef 100644
--- a/chrome/android/features/tab_ui/javatests/src/org/chromium/chrome/browser/tasks/tab_management/TabListViewHolderTest.java
+++ b/chrome/android/features/tab_ui/javatests/src/org/chromium/chrome/browser/tasks/tab_management/TabListViewHolderTest.java
@@ -861,29 +861,39 @@
 
     @Test
     @MediumTest
-    @UiThreadTest
     public void testPriceDropEndToEnd() {
-        ShoppingPersistedTabData.enablePriceTrackingWithOptimizationGuideForTesting();
-        ShoppingPersistedTabData.onDeferredStartup();
-        PersistedTabDataConfiguration.setUseTestConfig(true);
-        setPriceTrackingEnabledForTesting(true);
-        mockCurrencyFormatter();
-        mockUrlUtilities();
-        mockOptimizationGuideResponse(OptimizationGuideDecision.TRUE, ANY_PRICE_TRACKING_DATA);
-        MockTab tab = (MockTab) MockTab.createAndInitialize(1, false);
-        tab.setGurlOverrideForTesting(TEST_GURL);
-        tab.setIsInitialized(true);
-        CriticalPersistedTabData.from(tab).setTimestampMillis(System.currentTimeMillis());
-        TabListMediator.ShoppingPersistedTabDataFetcher fetcher =
-                new TabListMediator.ShoppingPersistedTabDataFetcher(tab, null);
-        mGridModel.set(TabProperties.SHOPPING_PERSISTED_TAB_DATA_FETCHER, fetcher);
-        testGridSelected(mTabGridView, mGridModel);
-        PriceCardView priceCardView = mTabGridView.findViewById(R.id.price_info_box_outer);
-        TextView currentPrice = mTabGridView.findViewById(R.id.current_price);
-        TextView previousPrice = mTabGridView.findViewById(R.id.previous_price);
-        Assert.assertEquals(EXPECTED_PRICE, currentPrice.getText());
-        Assert.assertEquals(EXPECTED_PREVIOUS_PRICE, previousPrice.getText());
-        Assert.assertEquals(EXPECTED_CONTENT_DESCRIPTION, priceCardView.getContentDescription());
+        TestThreadUtils.runOnUiThreadBlocking(() -> {
+            ShoppingPersistedTabData.onDeferredStartup();
+            ShoppingPersistedTabData.enablePriceTrackingWithOptimizationGuideForTesting();
+            PersistedTabDataConfiguration.setUseTestConfig(true);
+            setPriceTrackingEnabledForTesting(true);
+            mockCurrencyFormatter();
+            mockUrlUtilities();
+            mockOptimizationGuideResponse(OptimizationGuideDecision.TRUE, ANY_PRICE_TRACKING_DATA);
+            MockTab tab = (MockTab) MockTab.createAndInitialize(1, false);
+            tab.setGurlOverrideForTesting(TEST_GURL);
+            tab.setIsInitialized(true);
+            CriticalPersistedTabData.from(tab).setTimestampMillis(System.currentTimeMillis());
+            TabListMediator.ShoppingPersistedTabDataFetcher fetcher =
+                    new TabListMediator.ShoppingPersistedTabDataFetcher(tab, null);
+            mGridModel.set(TabProperties.SHOPPING_PERSISTED_TAB_DATA_FETCHER, fetcher);
+            testGridSelected(mTabGridView, mGridModel);
+        });
+        CriteriaHelper.pollUiThread(
+                ()
+                        -> EXPECTED_PRICE.equals(
+                                ((TextView) mTabGridView.findViewById(R.id.current_price))
+                                        .getText()));
+        CriteriaHelper.pollUiThread(
+                ()
+                        -> EXPECTED_PREVIOUS_PRICE.equals(
+                                ((TextView) mTabGridView.findViewById(R.id.previous_price))
+                                        .getText()));
+        CriteriaHelper.pollUiThread(()
+                                            -> EXPECTED_CONTENT_DESCRIPTION.equals(
+                                                    ((PriceCardView) mTabGridView.findViewById(
+                                                             R.id.price_info_box_outer))
+                                                            .getContentDescription()));
     }
 
     private void mockCurrencyFormatter() {
diff --git a/chrome/android/feed/core/javatests/src/org/chromium/chrome/browser/feed/v2/FeedV2TestHelper.java b/chrome/android/feed/core/javatests/src/org/chromium/chrome/browser/feed/v2/FeedV2TestHelper.java
index 4ceecff..bffddfe4 100644
--- a/chrome/android/feed/core/javatests/src/org/chromium/chrome/browser/feed/v2/FeedV2TestHelper.java
+++ b/chrome/android/feed/core/javatests/src/org/chromium/chrome/browser/feed/v2/FeedV2TestHelper.java
@@ -64,6 +64,7 @@
         enumNames.put("kClosedDialog", FeedUserActionType.CLOSED_DIALOG);
         enumNames.put("kShowSnackbar", FeedUserActionType.SHOW_SNACKBAR);
         enumNames.put("kOpenedNativeContextMenu", FeedUserActionType.OPENED_NATIVE_CONTEXT_MENU);
+        enumNames.put("kTappedFollowButton", FeedUserActionType.TAPPED_FOLLOW_BUTTON);
         return getEnumHistogramValues("ContentSuggestions.Feed.UserActions", enumNames);
     }
 
diff --git a/chrome/android/java/res/xml/google_services_preferences.xml b/chrome/android/java/res/xml/google_services_preferences.xml
index 380f674..df95c184 100644
--- a/chrome/android/java/res/xml/google_services_preferences.xml
+++ b/chrome/android/java/res/xml/google_services_preferences.xml
@@ -39,6 +39,11 @@
         android:title="@string/prefs_autofill_assistant_title"
         android:summary="@string/prefs_autofill_assistant_get_help_summary"
         android:persistent="false"/>
+    <org.chromium.components.browser_ui.settings.ChromeSwitchPreference
+        android:key="price_tracking_annotations"
+        android:title="@string/track_prices_on_tabs"
+        android:summary="@string/track_prices_on_tabs_description"
+        android:persistent="false"/>
     <org.chromium.components.browser_ui.settings.ChromeBasePreference
         android:key="autofill_assistant_subsection"
         android:title="@string/prefs_autofill_assistant_title"
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/sync/settings/GoogleServicesSettings.java b/chrome/android/java/src/org/chromium/chrome/browser/sync/settings/GoogleServicesSettings.java
index d3e8926..7cc5a75 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/sync/settings/GoogleServicesSettings.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/sync/settings/GoogleServicesSettings.java
@@ -29,6 +29,7 @@
 import org.chromium.chrome.browser.signin.services.IdentityServicesProvider;
 import org.chromium.chrome.browser.signin.services.SigninManager;
 import org.chromium.chrome.browser.signin.services.UnifiedConsentServiceBridge;
+import org.chromium.chrome.browser.tasks.tab_management.PriceTrackingUtilities;
 import org.chromium.chrome.browser.ui.signin.SignOutDialogFragment;
 import org.chromium.components.autofill_assistant.AssistantFeatures;
 import org.chromium.components.autofill_assistant.AutofillAssistantPreferencesUtil;
@@ -64,6 +65,8 @@
     public static final String PREF_AUTOFILL_ASSISTANT_SUBSECTION = "autofill_assistant_subsection";
     @VisibleForTesting
     public static final String PREF_METRICS_SETTINGS = "metrics_settings";
+    @VisibleForTesting
+    public static final String PREF_PRICE_TRACKING_ANNOTATIONS = "price_tracking_annotations";
 
     private final PrefService mPrefService = UserPrefs.get(Profile.getLastUsedRegularProfile());
     private final PrivacyPreferencesManagerImpl mPrivacyPrefManager =
@@ -75,6 +78,7 @@
     private ChromeSwitchPreference mSearchSuggestions;
     private ChromeSwitchPreference mUsageAndCrashReporting;
     private ChromeSwitchPreference mUrlKeyedAnonymizedData;
+    private ChromeSwitchPreference mPriceTrackingAnnotations;
     private @Nullable ChromeSwitchPreference mAutofillAssistant;
     private @Nullable Preference mContextualSearch;
 
@@ -137,6 +141,17 @@
             removePreference(getPreferenceScreen(), mContextualSearch);
             mContextualSearch = null;
         }
+
+        mPriceTrackingAnnotations =
+                (ChromeSwitchPreference) findPreference(PREF_PRICE_TRACKING_ANNOTATIONS);
+        if (!PriceTrackingUtilities.allowUsersToDisablePriceAnnotations()) {
+            removePreference(getPreferenceScreen(), mPriceTrackingAnnotations);
+            mPriceTrackingAnnotations = null;
+        } else {
+            mPriceTrackingAnnotations.setOnPreferenceChangeListener(this);
+            mPriceTrackingAnnotations.setManagedPreferenceDelegate(mManagedPreferenceDelegate);
+        }
+
         updatePreferences();
     }
 
@@ -208,6 +223,8 @@
                     Profile.getLastUsedRegularProfile(), (boolean) newValue);
         } else if (PREF_AUTOFILL_ASSISTANT.equals(key)) {
             setAutofillAssistantSwitchValue((boolean) newValue);
+        } else if (PREF_PRICE_TRACKING_ANNOTATIONS.equals(key)) {
+            PriceTrackingUtilities.setTrackPricesOnTabsEnabled((boolean) newValue);
         }
         return true;
     }
@@ -234,6 +251,10 @@
             mContextualSearch.setSummary(
                     isContextualSearchEnabled ? R.string.text_on : R.string.text_off);
         }
+        if (mPriceTrackingAnnotations != null) {
+            mPriceTrackingAnnotations.setChecked(
+                    PriceTrackingUtilities.isTrackPricesOnTabsEnabled());
+        }
     }
 
     private ChromeManagedPreferenceDelegate createManagedPreferenceDelegate() {
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/app/appmenu/OverviewAppMenuTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/app/appmenu/OverviewAppMenuTest.java
index 4977d018..6d0a65c 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/app/appmenu/OverviewAppMenuTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/app/appmenu/OverviewAppMenuTest.java
@@ -23,7 +23,6 @@
 import org.chromium.chrome.browser.flags.ChromeSwitches;
 import org.chromium.chrome.browser.layouts.LayoutTestUtils;
 import org.chromium.chrome.browser.layouts.LayoutType;
-import org.chromium.chrome.browser.tasks.tab_management.PriceTrackingUtilities;
 import org.chromium.chrome.browser.ui.appmenu.AppMenuTestSupport;
 import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
 import org.chromium.chrome.test.ChromeTabbedActivityTestRule;
@@ -198,138 +197,6 @@
                 mActivityTestRule.getAppMenuCoordinator(), R.id.menu_group_tabs));
     }
 
-    @Test
-    @SmallTest
-    @Feature({"Browser", "Main"})
-    @Features.EnableFeatures({ChromeFeatureList.COMMERCE_PRICE_TRACKING + "<Study"})
-    @Features.DisableFeatures({ChromeFeatureList.START_SURFACE_ANDROID})
-    @CommandLineFlags.Add({"force-fieldtrials=Study/Group",
-            "force-fieldtrial-params=Study.Group:enable_price_tracking/false"})
-    public void
-    testTrackPriceOnTabsIsDisabled() throws Exception {
-        TestThreadUtils.runOnUiThreadBlocking(() -> {
-            PriceTrackingUtilities.setIsSignedInAndSyncEnabledForTesting(true);
-            AppMenuTestSupport.showAppMenu(mActivityTestRule.getAppMenuCoordinator(), null, false);
-        });
-
-        assertNull(AppMenuTestSupport.getMenuItemPropertyModel(
-                mActivityTestRule.getAppMenuCoordinator(), R.id.track_prices_row_menu_id));
-    }
-
-    @Test
-    @SmallTest
-    @Feature({"Browser", "Main"})
-    @Features.EnableFeatures({ChromeFeatureList.COMMERCE_PRICE_TRACKING + "<Study"})
-    @Features.DisableFeatures({ChromeFeatureList.START_SURFACE_ANDROID})
-    @CommandLineFlags.Add({"force-fieldtrials=Study/Group",
-            "force-fieldtrial-params=Study.Group:enable_price_tracking/true"
-                    + "/allow_disable_price_annotations/true"})
-    public void
-    testTrackPriceOnTabsIsEnabled() throws Exception {
-        TestThreadUtils.runOnUiThreadBlocking(() -> {
-            PriceTrackingUtilities.setIsSignedInAndSyncEnabledForTesting(true);
-            AppMenuTestSupport.showAppMenu(mActivityTestRule.getAppMenuCoordinator(), null, false);
-        });
-
-        assertNotNull(AppMenuTestSupport.getMenuItemPropertyModel(
-                mActivityTestRule.getAppMenuCoordinator(), R.id.track_prices_row_menu_id));
-    }
-
-    @Test
-    @SmallTest
-    @Feature({"Browser", "Main"})
-    @Features.EnableFeatures({ChromeFeatureList.COMMERCE_PRICE_TRACKING + "<Study"})
-    @Features.DisableFeatures({ChromeFeatureList.START_SURFACE_ANDROID})
-    @CommandLineFlags.Add({"force-fieldtrials=Study/Group",
-            "force-fieldtrial-params=Study.Group:enable_price_tracking/true"
-                    + "/allow_disable_price_annotations/true"})
-    public void
-    testTrackPriceOnTabsIsDisabledInIncognitoMode() throws Exception {
-        TestThreadUtils.runOnUiThreadBlocking(() -> {
-            PriceTrackingUtilities.setIsSignedInAndSyncEnabledForTesting(true);
-            mActivityTestRule.getActivity().getTabModelSelector().selectModel(true);
-            AppMenuTestSupport.showAppMenu(mActivityTestRule.getAppMenuCoordinator(), null, false);
-        });
-
-        assertNull(AppMenuTestSupport.getMenuItemPropertyModel(
-                mActivityTestRule.getAppMenuCoordinator(), R.id.track_prices_row_menu_id));
-    }
-
-    @Test
-    @SmallTest
-    @Feature({"Browser", "Main"})
-    @Features.EnableFeatures({ChromeFeatureList.COMMERCE_PRICE_TRACKING + "<Study"})
-    @Features.DisableFeatures({ChromeFeatureList.START_SURFACE_ANDROID})
-    @CommandLineFlags.Add({"force-fieldtrials=Study/Group",
-            "force-fieldtrial-params=Study.Group:enable_price_tracking/true"
-                    + "/allow_disable_price_annotations/true"})
-    public void
-    testTrackPriceOnTabsIsDisabledIfSyncDisabledOrNotSignedIn() throws Exception {
-        TestThreadUtils.runOnUiThreadBlocking(() -> {
-            PriceTrackingUtilities.setIsSignedInAndSyncEnabledForTesting(false);
-            AppMenuTestSupport.showAppMenu(mActivityTestRule.getAppMenuCoordinator(), null, false);
-        });
-
-        assertNull(AppMenuTestSupport.getMenuItemPropertyModel(
-                mActivityTestRule.getAppMenuCoordinator(), R.id.track_prices_row_menu_id));
-    }
-
-    @Test
-    @SmallTest
-    @Feature({"Browser", "Main"})
-    @Features.EnableFeatures({ChromeFeatureList.COMMERCE_PRICE_TRACKING + "<Study"})
-    @Features.DisableFeatures({ChromeFeatureList.START_SURFACE_ANDROID})
-    @CommandLineFlags.Add({"force-fieldtrials=Study/Group",
-            "force-fieldtrial-params=Study.Group:enable_price_tracking/true"
-                    + "/allow_disable_price_annotations/false/enable_price_notification/false"})
-    public void
-    testTrackPriceOnTabsIsDisabledIfNoSettingsAvailable() throws Exception {
-        TestThreadUtils.runOnUiThreadBlocking(() -> {
-            PriceTrackingUtilities.setIsSignedInAndSyncEnabledForTesting(true);
-            AppMenuTestSupport.showAppMenu(mActivityTestRule.getAppMenuCoordinator(), null, false);
-        });
-
-        assertNull(AppMenuTestSupport.getMenuItemPropertyModel(
-                mActivityTestRule.getAppMenuCoordinator(), R.id.track_prices_row_menu_id));
-    }
-
-    @Test
-    @SmallTest
-    @Feature({"Browser", "Main"})
-    @Features.EnableFeatures({ChromeFeatureList.START_SURFACE_ANDROID,
-            ChromeFeatureList.COMMERCE_PRICE_TRACKING + "<Study"})
-    @CommandLineFlags.Add({"force-fieldtrials=Study/Group",
-            "force-fieldtrial-params=Study.Group:enable_price_tracking/false"})
-    public void
-    testTrackPriceOnTabsIsDisabledWithStartSurface() throws Exception {
-        TestThreadUtils.runOnUiThreadBlocking(() -> {
-            PriceTrackingUtilities.setIsSignedInAndSyncEnabledForTesting(true);
-            AppMenuTestSupport.showAppMenu(mActivityTestRule.getAppMenuCoordinator(), null, false);
-        });
-
-        assertNull(AppMenuTestSupport.getMenuItemPropertyModel(
-                mActivityTestRule.getAppMenuCoordinator(), R.id.track_prices_row_menu_id));
-    }
-
-    @Test
-    @SmallTest
-    @Feature({"Browser", "Main"})
-    @Features.EnableFeatures({ChromeFeatureList.START_SURFACE_ANDROID,
-            ChromeFeatureList.COMMERCE_PRICE_TRACKING + "<Study"})
-    @CommandLineFlags.Add({"force-fieldtrials=Study/Group",
-            "force-fieldtrial-params=Study.Group:enable_price_tracking/true"
-                    + "/allow_disable_price_annotations/true"})
-    public void
-    testTrackPriceOnTabsIsEnabledWithStartSurface() throws Exception {
-        TestThreadUtils.runOnUiThreadBlocking(() -> {
-            PriceTrackingUtilities.setIsSignedInAndSyncEnabledForTesting(true);
-            AppMenuTestSupport.showAppMenu(mActivityTestRule.getAppMenuCoordinator(), null, false);
-        });
-
-        assertNotNull(AppMenuTestSupport.getMenuItemPropertyModel(
-                mActivityTestRule.getAppMenuCoordinator(), R.id.track_prices_row_menu_id));
-    }
-
     private void verifyTabSwitcherMenu() {
         assertNotNull(AppMenuTestSupport.getMenuItemPropertyModel(
                 mActivityTestRule.getAppMenuCoordinator(), R.id.new_tab_menu_id));
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/autofill/settings/AutofillPaymentMethodsFragmentTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/autofill/settings/AutofillPaymentMethodsFragmentTest.java
index 2819b2c9..5859d32e 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/autofill/settings/AutofillPaymentMethodsFragmentTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/autofill/settings/AutofillPaymentMethodsFragmentTest.java
@@ -21,7 +21,6 @@
 import org.chromium.base.test.util.Batch;
 import org.chromium.chrome.browser.autofill.AutofillTestHelper;
 import org.chromium.chrome.browser.autofill.PersonalDataManager.CreditCard;
-import org.chromium.chrome.browser.flags.ChromeFeatureList;
 import org.chromium.chrome.browser.settings.SettingsActivity;
 import org.chromium.chrome.browser.settings.SettingsActivityTestRule;
 import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
@@ -133,51 +132,6 @@
         assertThat(title).contains("1111");
     }
 
-    @Test
-    @MediumTest
-    @Features.EnableFeatures({ChromeFeatureList.AUTOFILL_ENABLE_GOOGLE_ISSUED_CARD})
-    public void testGoogleIssuedServerCard_displaysGoogleSpecificTitle() throws Exception {
-        mAutofillTestHelper.addServerCreditCard(
-                SAMPLE_CARD_VISA, /* nickname= */ "", CARD_ISSUER_GOOGLE);
-
-        SettingsActivity activity = mSettingsActivityTestRule.startSettingsActivity();
-
-        Preference cardPreference = getPreferenceScreen(activity).getPreference(1);
-        String title = cardPreference.getTitle().toString();
-        assertThat(title).contains("Plex Visa");
-        assertThat(title).contains("1111");
-    }
-
-    @Test
-    @MediumTest
-    @Features.DisableFeatures({ChromeFeatureList.AUTOFILL_ENABLE_GOOGLE_ISSUED_CARD})
-    public void testGoogleIssuedServerCard_expOff_cardNotDisplayed() throws Exception {
-        mAutofillTestHelper.addServerCreditCard(
-                SAMPLE_CARD_VISA, /* nickname= */ "", CARD_ISSUER_GOOGLE);
-
-        SettingsActivity activity = mSettingsActivityTestRule.startSettingsActivity();
-
-        // Verify that the preferences on the initial screen map to Save and Fill toggle + Add Card
-        // button + Payment Apps.
-        Assert.assertEquals(3, getPreferenceScreen(activity).getPreferenceCount());
-    }
-
-    @Test
-    @MediumTest
-    @Features.EnableFeatures({ChromeFeatureList.AUTOFILL_ENABLE_GOOGLE_ISSUED_CARD})
-    public void testGoogleIssuedServerCardWithNickname_displaysNicknameAndLastFourAsTitle()
-            throws Exception {
-        mAutofillTestHelper.addServerCreditCard(
-                SAMPLE_CARD_VISA, "Test nickname", CARD_ISSUER_GOOGLE);
-
-        SettingsActivity activity = mSettingsActivityTestRule.startSettingsActivity();
-
-        Preference cardPreference = getPreferenceScreen(activity).getPreference(1);
-        String title = cardPreference.getTitle().toString();
-        assertThat(title).contains("Test nickname");
-        assertThat(title).contains("1111");
-    }
-
     private static PreferenceScreen getPreferenceScreen(SettingsActivity activity) {
         return ((AutofillPaymentMethodsFragment) activity.getMainFragment()).getPreferenceScreen();
     }
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/sync/GoogleServicesSettingsTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/sync/GoogleServicesSettingsTest.java
index 2d3a2640..cf57b50d 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/sync/GoogleServicesSettingsTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/sync/GoogleServicesSettingsTest.java
@@ -30,6 +30,7 @@
 import org.chromium.chrome.browser.settings.SettingsActivityTestRule;
 import org.chromium.chrome.browser.signin.services.IdentityServicesProvider;
 import org.chromium.chrome.browser.sync.settings.GoogleServicesSettings;
+import org.chromium.chrome.browser.tasks.tab_management.PriceTrackingUtilities;
 import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
 import org.chromium.chrome.test.ChromeTabbedActivityTestRule;
 import org.chromium.chrome.test.util.browser.Features.DisableFeatures;
@@ -329,6 +330,74 @@
         });
     }
 
+    @Test
+    @LargeTest
+    @Feature({"Preference"})
+    @EnableFeatures({ChromeFeatureList.COMMERCE_PRICE_TRACKING + "<Study"})
+    @CommandLineFlags.Add({"force-fieldtrials=Study/Group",
+            "force-fieldtrial-params=Study.Group:enable_price_tracking/true"
+                    + "/allow_disable_price_annotations/true"})
+    public void
+    testPriceTrackingAnnotations() {
+        TestThreadUtils.runOnUiThreadBlocking(
+                () -> PriceTrackingUtilities.setIsSignedInAndSyncEnabledForTesting(true));
+
+        final GoogleServicesSettings googleServicesSettings = startGoogleServicesSettings();
+
+        TestThreadUtils.runOnUiThreadBlocking(() -> {
+            ChromeSwitchPreference priceAnnotationsSwitch =
+                    (ChromeSwitchPreference) googleServicesSettings.findPreference(
+                            GoogleServicesSettings.PREF_PRICE_TRACKING_ANNOTATIONS);
+            Assert.assertTrue(priceAnnotationsSwitch.isVisible());
+            Assert.assertTrue(priceAnnotationsSwitch.isChecked());
+
+            priceAnnotationsSwitch.performClick();
+            Assert.assertFalse(PriceTrackingUtilities.isTrackPricesOnTabsEnabled());
+            priceAnnotationsSwitch.performClick();
+            Assert.assertTrue(PriceTrackingUtilities.isTrackPricesOnTabsEnabled());
+        });
+    }
+
+    @Test
+    @LargeTest
+    @Feature({"Preference"})
+    @EnableFeatures({ChromeFeatureList.COMMERCE_PRICE_TRACKING + "<Study"})
+    @CommandLineFlags.Add({"force-fieldtrials=Study/Group",
+            "force-fieldtrial-params=Study.Group:enable_price_tracking/true"
+                    + "/allow_disable_price_annotations/false"})
+    public void
+    testPriceTrackingAnnotations_FeatureDisabled() {
+        TestThreadUtils.runOnUiThreadBlocking(
+                () -> PriceTrackingUtilities.setIsSignedInAndSyncEnabledForTesting(true));
+
+        final GoogleServicesSettings googleServicesSettings = startGoogleServicesSettings();
+
+        TestThreadUtils.runOnUiThreadBlocking(() -> {
+            Assert.assertNull(googleServicesSettings.findPreference(
+                    GoogleServicesSettings.PREF_PRICE_TRACKING_ANNOTATIONS));
+        });
+    }
+
+    @Test
+    @LargeTest
+    @Feature({"Preference"})
+    @EnableFeatures({ChromeFeatureList.COMMERCE_PRICE_TRACKING + "<Study"})
+    @CommandLineFlags.Add({"force-fieldtrials=Study/Group",
+            "force-fieldtrial-params=Study.Group:enable_price_tracking/true"
+                    + "/allow_disable_price_annotations/true"})
+    public void
+    testPriceTrackingAnnotations_NotSignedIn() {
+        TestThreadUtils.runOnUiThreadBlocking(
+                () -> PriceTrackingUtilities.setIsSignedInAndSyncEnabledForTesting(false));
+
+        final GoogleServicesSettings googleServicesSettings = startGoogleServicesSettings();
+
+        TestThreadUtils.runOnUiThreadBlocking(() -> {
+            Assert.assertNull(googleServicesSettings.findPreference(
+                    GoogleServicesSettings.PREF_PRICE_TRACKING_ANNOTATIONS));
+        });
+    }
+
     private void setAutofillAssistantSwitchValue(boolean newValue) {
         AutofillAssistantPreferencesUtil.setAssistantEnabledPreference(newValue);
     }
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/tab/state/ShoppingPersistedTabDataDeferredStartupTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/tab/state/ShoppingPersistedTabDataDeferredStartupTest.java
index 0124be1..577379f 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/tab/state/ShoppingPersistedTabDataDeferredStartupTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/tab/state/ShoppingPersistedTabDataDeferredStartupTest.java
@@ -129,9 +129,10 @@
             });
         });
         ShoppingPersistedTabDataTestUtils.acquireSemaphore(semaphore);
+        TestThreadUtils.runOnUiThreadBlocking(
+                () -> { ShoppingPersistedTabData.onDeferredStartup(); });
         final Semaphore newSemaphore = new Semaphore(0);
         TestThreadUtils.runOnUiThreadBlocking(() -> {
-            ShoppingPersistedTabData.onDeferredStartup();
             ShoppingPersistedTabData.from(tab, (shoppingPersistedTabData) -> {
                 Assert.assertNotNull(shoppingPersistedTabData);
                 Assert.assertEquals(ShoppingPersistedTabDataTestUtils.UPDATED_PRICE_MICROS,
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/tab/state/ShoppingPersistedTabDataLegacyTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/tab/state/ShoppingPersistedTabDataLegacyTest.java
index 84727414a..c2ba08c 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/tab/state/ShoppingPersistedTabDataLegacyTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/tab/state/ShoppingPersistedTabDataLegacyTest.java
@@ -161,7 +161,7 @@
         ShoppingPersistedTabDataTestUtils.acquireSemaphore(updateSemaphore);
         return ShoppingPersistedTabDataTestUtils.getTimeLastUpdatedOnUiThread(tab);
     }
-    @UiThreadTest
+
     @SmallTest
     @Test
     @CommandLineFlags.
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/tab/state/ShoppingPersistedTabDataTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/tab/state/ShoppingPersistedTabDataTest.java
index e324bd7..f623602 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/tab/state/ShoppingPersistedTabDataTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/tab/state/ShoppingPersistedTabDataTest.java
@@ -295,7 +295,6 @@
                 mOptimizationGuideBridgeJniMock, 1);
     }
 
-    @UiThreadTest
     @SmallTest
     @Test
     @CommandLineFlags.
@@ -593,7 +592,6 @@
         Assert.assertNull(shoppingPersistedTabData.getPriceDrop());
     }
 
-    @UiThreadTest
     @SmallTest
     @Test
     @CommandLineFlags.
@@ -617,7 +615,6 @@
         ShoppingPersistedTabDataTestUtils.acquireSemaphore(semaphore);
     }
 
-    @UiThreadTest
     @SmallTest
     @Test
     @CommandLineFlags.
@@ -641,7 +638,6 @@
         ShoppingPersistedTabDataTestUtils.acquireSemaphore(semaphore);
     }
 
-    @UiThreadTest
     @SmallTest
     @Test
     @CommandLineFlags.
@@ -664,7 +660,6 @@
         ShoppingPersistedTabDataTestUtils.acquireSemaphore(semaphore);
     }
 
-    @UiThreadTest
     @SmallTest
     @Test
     public void testSPTDNullOptimizationGuideFalse() {
@@ -849,30 +844,32 @@
         Assert.assertTrue(shoppingPersistedTabData.needsUpdate());
     }
 
-    @UiThreadTest
     @SmallTest
     @Test
     public void testIncognitoTabDisabled() throws TimeoutException {
         TabImpl tab = mock(TabImpl.class);
         doReturn(true).when(tab).isIncognito();
         CallbackHelper callbackHelper = new CallbackHelper();
-        ShoppingPersistedTabData.from(tab, (res) -> {
-            Assert.assertNull(res);
-            callbackHelper.notifyCalled();
+        TestThreadUtils.runOnUiThreadBlocking(() -> {
+            ShoppingPersistedTabData.from(tab, (res) -> {
+                Assert.assertNull(res);
+                callbackHelper.notifyCalled();
+            });
         });
         callbackHelper.waitForCallback(0);
     }
 
-    @UiThreadTest
     @SmallTest
     @Test
     public void testCustomTabsDisabled() throws TimeoutException {
         TabImpl tab = mock(TabImpl.class);
         doReturn(true).when(tab).isCustomTab();
         CallbackHelper callbackHelper = new CallbackHelper();
-        ShoppingPersistedTabData.from(tab, (res) -> {
-            Assert.assertNull(res);
-            callbackHelper.notifyCalled();
+        TestThreadUtils.runOnUiThreadBlocking(() -> {
+            ShoppingPersistedTabData.from(tab, (res) -> {
+                Assert.assertNull(res);
+                callbackHelper.notifyCalled();
+            });
         });
         callbackHelper.waitForCallback(0);
     }
diff --git a/chrome/app/chrome_command_ids.h b/chrome/app/chrome_command_ids.h
index e046c85..a65dc7d 100644
--- a/chrome/app/chrome_command_ids.h
+++ b/chrome/app/chrome_command_ids.h
@@ -410,6 +410,7 @@
 #if BUILDFLAG(GOOGLE_CHROME_BRANDING)
 #define IDC_MEDIA_TOOLBAR_CONTEXT_REPORT_CAST_ISSUE 51209
 #endif
+#define IDC_MEDIA_TOOLBAR_CONTEXT_SHOW_OTHER_SESSIONS 51210
 
 // Context menu items for media stream status tray
 #define IDC_MEDIA_STREAM_DEVICE_STATUS_TRAY 51300
diff --git a/chrome/app/generated_resources.grd b/chrome/app/generated_resources.grd
index ba98555..36e3e64b 100644
--- a/chrome/app/generated_resources.grd
+++ b/chrome/app/generated_resources.grd
@@ -5118,7 +5118,10 @@
       <!-- End Extension Settings Overridden Dialog strings. -->
       <!-- Force Installed Deprecated Apps Deletion Dialog strings. -->
       <message name="IDS_FORCE_INSTALLED_DEPRECATED_APPS_CONTENT" desc="Content of the force installed deprecated app dialog">
-        Your administrator installed "<ph name="EXTENSION_NAME">$1<ex>Google Dos</ex></ph>" but this Chrome App is no longer supported. Contact your administrator to remove it. <ph name="LEARN_MORE">$2<ex>Learn more</ex></ph>
+        Your administrator installed "<ph name="EXTENSION_NAME">$1<ex>Google Dos</ex></ph>" but this Chrome App is no longer supported. Contact your administrator to remove it.
+      </message>
+      <message name="IDS_FORCE_INSTALLED_DEPRECATED_APPS_LEARN_MORE_AX_LABEL" desc="Accessibility label text for IDS_DEPRECATED_APPS_LEARN_MORE link">
+        Learn more about unsupported Chrome Apps
       </message>
       <!-- End Force Installed Deprecated Apps Deletion Dialog strings. -->
       <!-- Deprecated Apps Deletion Dialog strings. -->
diff --git a/chrome/app/generated_resources_grd/IDS_FORCE_INSTALLED_DEPRECATED_APPS_CONTENT.png.sha1 b/chrome/app/generated_resources_grd/IDS_FORCE_INSTALLED_DEPRECATED_APPS_CONTENT.png.sha1
index d217217..8ec600b 100644
--- a/chrome/app/generated_resources_grd/IDS_FORCE_INSTALLED_DEPRECATED_APPS_CONTENT.png.sha1
+++ b/chrome/app/generated_resources_grd/IDS_FORCE_INSTALLED_DEPRECATED_APPS_CONTENT.png.sha1
@@ -1 +1 @@
-aadc71c61061278c7c23c480cf863c2004f48caa
\ No newline at end of file
+767a804ffadd4122174e17e900980fc7eb77f42b
\ No newline at end of file
diff --git a/chrome/app/generated_resources_grd/IDS_FORCE_INSTALLED_DEPRECATED_APPS_LEARN_MORE_AX_LABEL.png.sha1 b/chrome/app/generated_resources_grd/IDS_FORCE_INSTALLED_DEPRECATED_APPS_LEARN_MORE_AX_LABEL.png.sha1
new file mode 100644
index 0000000..8ec600b
--- /dev/null
+++ b/chrome/app/generated_resources_grd/IDS_FORCE_INSTALLED_DEPRECATED_APPS_LEARN_MORE_AX_LABEL.png.sha1
@@ -0,0 +1 @@
+767a804ffadd4122174e17e900980fc7eb77f42b
\ No newline at end of file
diff --git a/chrome/app/gmc_strings.grdp b/chrome/app/gmc_strings.grdp
index 4ccedf1..c4c0f57 100644
--- a/chrome/app/gmc_strings.grdp
+++ b/chrome/app/gmc_strings.grdp
@@ -41,7 +41,9 @@
    Control the media you're casting
   </message>
   <message name="IDS_MEDIA_TOOLBAR_CONTEXT_REPORT_CAST_ISSUE" desc="Title of the toolbar button's context menu item, which, on click, opens a page to report an issue with Cast.">
-    Report an issue with Google Cast
+   Report an issue with Google Cast
   </message>
-
+  <message name="IDS_MEDIA_TOOLBAR_CONTEXT_SHOW_OTHER_SESSIONS" desc="Title of a menu item which, on click, toggles (shows or hides) Cast sessions started by other devices on the same network.">
+   Show other Cast sessions
+  </message>
 </grit-part>
diff --git a/chrome/app/gmc_strings_grdp/IDS_MEDIA_TOOLBAR_CONTEXT_SHOW_OTHER_SESSIONS.png.sha1 b/chrome/app/gmc_strings_grdp/IDS_MEDIA_TOOLBAR_CONTEXT_SHOW_OTHER_SESSIONS.png.sha1
new file mode 100644
index 0000000..e206e36
--- /dev/null
+++ b/chrome/app/gmc_strings_grdp/IDS_MEDIA_TOOLBAR_CONTEXT_SHOW_OTHER_SESSIONS.png.sha1
@@ -0,0 +1 @@
+0dacfcd0f5492038e54bb18e493b299a3e8c10cb
\ No newline at end of file
diff --git a/chrome/app/profiles_strings.grdp b/chrome/app/profiles_strings.grdp
index 6a1fe31..ae5d1b2c 100644
--- a/chrome/app/profiles_strings.grdp
+++ b/chrome/app/profiles_strings.grdp
@@ -522,9 +522,15 @@
     <message name="IDS_ENTERPRISE_WELCOME_PROFILE_REQUIRED_TITLE" desc="Title of the enteprise profile welcome screen when a profile is required. It is shown after a user signs into a managed account outside of the profile creation flow.">
       Your organization requires a profile
     </message>
+    <message name="IDS_ENTERPRISE_WELCOME_PROFILE_WILL_BE_MANAGED_TITLE" desc="Title of the enteprise profile welcome screen when a managed profile will be created. It is shown after a user signs into a managed account outside of the profile creation flow.">
+      Your organization will manage this profile
+    </message>
     <message name="IDS_ENTERPRISE_PROFILE_WELCOME_CREATE_PROFILE_BUTTON" desc="Label of the proceed button on the enterprise profile welcome screen when a user signed in an enterprise account outside of the profile creation flow. The label informs the user that a profile will be created.">
       Create
     </message>
+    <message name="IDS_ENTERPRISE_PROFILE_WELCOME_LINK_DATA_CHECKBOX" desc="Label of the checkbox on the enterprise profile welcome screen that allows a user to link existing data to the new profile. The label informs the user that a profile will be created.">
+      Add existing browsing data (bookmarks, passwords, history) to profile
+    </message>
 
     <!-- Profile Picker -->
     <message name="IDS_PROFILE_PICKER_ADD_SPACE_BUTTON" desc="Text for the add space button on the profile picker main view">
diff --git a/chrome/app/profiles_strings_grdp/IDS_ENTERPRISE_PROFILE_WELCOME_LINK_DATA_CHECKBOX.png.sha1 b/chrome/app/profiles_strings_grdp/IDS_ENTERPRISE_PROFILE_WELCOME_LINK_DATA_CHECKBOX.png.sha1
new file mode 100644
index 0000000..3c3d951
--- /dev/null
+++ b/chrome/app/profiles_strings_grdp/IDS_ENTERPRISE_PROFILE_WELCOME_LINK_DATA_CHECKBOX.png.sha1
@@ -0,0 +1 @@
+7333620e29370717085ba5a843c35e9c81012d83
\ No newline at end of file
diff --git a/chrome/app/profiles_strings_grdp/IDS_ENTERPRISE_WELCOME_PROFILE_WILL_BE_MANAGED_TITLE.png.sha1 b/chrome/app/profiles_strings_grdp/IDS_ENTERPRISE_WELCOME_PROFILE_WILL_BE_MANAGED_TITLE.png.sha1
new file mode 100644
index 0000000..3c3d951
--- /dev/null
+++ b/chrome/app/profiles_strings_grdp/IDS_ENTERPRISE_WELCOME_PROFILE_WILL_BE_MANAGED_TITLE.png.sha1
@@ -0,0 +1 @@
+7333620e29370717085ba5a843c35e9c81012d83
\ No newline at end of file
diff --git a/chrome/browser/BUILD.gn b/chrome/browser/BUILD.gn
index db6fc6a..30281f61 100644
--- a/chrome/browser/BUILD.gn
+++ b/chrome/browser/BUILD.gn
@@ -808,6 +808,8 @@
     "metrics/variations/chrome_variations_service_client.cc",
     "metrics/variations/chrome_variations_service_client.h",
     "native_window_notification_source.h",
+    "navigation_predictor/anchor_element_preloader.cc",
+    "navigation_predictor/anchor_element_preloader.h",
     "navigation_predictor/navigation_predictor.cc",
     "navigation_predictor/navigation_predictor.h",
     "navigation_predictor/navigation_predictor_features.cc",
diff --git a/chrome/browser/about_flags.cc b/chrome/browser/about_flags.cc
index 26f5a34..3ed4057 100644
--- a/chrome/browser/about_flags.cc
+++ b/chrome/browser/about_flags.cc
@@ -3215,6 +3215,9 @@
     {"ash-bento-bar", flag_descriptions::kBentoBarName,
      flag_descriptions::kBentoBarDescription, kOsCrOS,
      FEATURE_VALUE_TYPE(ash::features::kBentoBar)},
+    {"ash-capture-mode-selfie-cam", flag_descriptions::kCaptureSelfieCamName,
+     flag_descriptions::kCaptureSelfieCamDescription, kOsCrOS,
+     FEATURE_VALUE_TYPE(ash::features::kCaptureModeSelfieCamera)},
     {"ash-drag-window-to-new-desk", flag_descriptions::kDragWindowToNewDeskName,
      flag_descriptions::kDragWindowToNewDeskDescription, kOsCrOS,
      FEATURE_VALUE_TYPE(ash::features::kDragWindowToNewDesk)},
@@ -4728,9 +4731,6 @@
     {"enable-universal-links", flag_descriptions::kEnableUniversalLinksName,
      flag_descriptions::kEnableUniversalLinksDescription, kOsMac,
      FEATURE_VALUE_TYPE(features::kEnableUniveralLinks)},
-    {"new-usb-backend", flag_descriptions::kNewUsbBackendName,
-     flag_descriptions::kNewUsbBackendDescription, kOsMac,
-     FEATURE_VALUE_TYPE(device::kNewUsbBackend)},
 #endif  // BUILDFLAG(IS_MAC)
 
 #if BUILDFLAG(IS_ANDROID)
@@ -6841,11 +6841,6 @@
      flag_descriptions::kEnableExperimentalCookieFeaturesDescription, kOsAll,
      MULTI_VALUE_TYPE(kEnableExperimentalCookieFeaturesChoices)},
 
-    {"autofill-enable-google-issued-card",
-     flag_descriptions::kAutofillEnableGoogleIssuedCardName,
-     flag_descriptions::kAutofillEnableGoogleIssuedCardDescription, kOsAll,
-     FEATURE_VALUE_TYPE(autofill::features::kAutofillEnableGoogleIssuedCard)},
-
     {"permission-chip", flag_descriptions::kPermissionChipName,
      flag_descriptions::kPermissionChipDescription, kOsDesktop,
      FEATURE_VALUE_TYPE(permissions::features::kPermissionChip)},
@@ -7170,8 +7165,8 @@
      // Use a command-line parameter instead of a FEATURE_VALUE_TYPE to enable
      // multiple related features when they are available.
      SINGLE_VALUE_TYPE_AND_VALUE(switches::kEnableFeatures,
-                                 "PrivacySandboxAdsAPIsOverride"
-                                 "Fledge,BrowsingTopics,ConversionMeasurement"
+                                 "PrivacySandboxAdsAPIsOverride,"
+                                 "Fledge,BrowsingTopics,ConversionMeasurement,"
                                  "OverridePrivacySandboxSettingsLocalTesting")},
 
     {"animated-image-resume", flag_descriptions::kAnimatedImageResumeName,
diff --git a/chrome/browser/android/resource_id.h b/chrome/browser/android/resource_id.h
index 7e9cba9..84afa632 100644
--- a/chrome/browser/android/resource_id.h
+++ b/chrome/browser/android/resource_id.h
@@ -83,7 +83,6 @@
 LINK_RESOURCE_ID(IDR_AUTOFILL_CC_TROY, R.drawable.troy_card)
 LINK_RESOURCE_ID(IDR_AUTOFILL_CC_UNIONPAY, R.drawable.unionpay_card)
 LINK_RESOURCE_ID(IDR_AUTOFILL_CC_VISA, R.drawable.visa_card)
-LINK_RESOURCE_ID(IDR_AUTOFILL_GOOGLE_ISSUED_CARD, R.drawable.google_pay_plex)
 LINK_RESOURCE_ID(IDR_AUTOFILL_GOOGLE_PAY, R.drawable.google_pay)
 // Use DECLARE_RESOURCE_ID here as these resources are used for android only.
 DECLARE_RESOURCE_ID(IDR_ANDROID_AUTOFILL_CC_SCAN_NEW,
diff --git a/chrome/browser/apps/app_service/publishers/arc_apps.cc b/chrome/browser/apps/app_service/publishers/arc_apps.cc
index c0e93a2..eb15eea 100644
--- a/chrome/browser/apps/app_service/publishers/arc_apps.cc
+++ b/chrome/browser/apps/app_service/publishers/arc_apps.cc
@@ -1474,6 +1474,67 @@
   notification_observation_.Reset();
 }
 
+void ArcApps::OnPrivacyItemsChanged(
+    std::vector<arc::mojom::PrivacyItemPtr> privacy_items) {
+  ArcAppListPrefs* prefs = ArcAppListPrefs::Get(profile_);
+  if (!prefs) {
+    return;
+  }
+
+  // Get the existing accessing app ids from `accessing_apps_`, and set all of
+  // them as false to explicitly update `AppCapabilityAccessCache` to ensure the
+  // access is stopped when they are not list in `privacy_items`. If they are
+  // still accessing, they will exist in `privacy_items`, and be set as true in
+  // the next loop for `privacy_items`.
+  base::flat_map<std::string, apps::mojom::CapabilityAccessPtr>
+      capability_accesses;
+  for (const auto& app_id : accessing_apps_) {
+    auto access = apps::mojom::CapabilityAccess::New();
+    access->app_id = app_id;
+    access->camera = apps::mojom::OptionalBool::kFalse;
+    access->microphone = apps::mojom::OptionalBool::kFalse;
+    capability_accesses[app_id] = std::move(access);
+  }
+  accessing_apps_.clear();
+
+  // Check the new items in `privacy_items`, and update `capability_accesses` to
+  // set the access item as true, if the camera or the microphone is still in
+  // use.
+  for (const auto& item : privacy_items) {
+    arc::mojom::AppPermissionGroup permission = item->permission_group;
+    if (permission != arc::mojom::AppPermissionGroup::CAMERA &&
+        permission != arc::mojom::AppPermissionGroup::MICROPHONE) {
+      continue;
+    }
+
+    auto package_name = item->privacy_application->package_name;
+    for (const auto& app_id : prefs->GetAppsForPackage(package_name)) {
+      accessing_apps_.insert(app_id);
+      auto it = capability_accesses.find(app_id);
+      if (it == capability_accesses.end()) {
+        capability_accesses[app_id] = apps::mojom::CapabilityAccess::New();
+        it = capability_accesses.find(app_id);
+        it->second->app_id = app_id;
+      }
+      if (permission == arc::mojom::AppPermissionGroup::CAMERA) {
+        it->second->camera = apps::mojom::OptionalBool::kTrue;
+      }
+      if (permission == arc::mojom::AppPermissionGroup::MICROPHONE) {
+        it->second->microphone = apps::mojom::OptionalBool::kTrue;
+      }
+    }
+  }
+
+  // Write the record to `AppCapabilityAccessCache`.
+  for (auto& subscriber : subscribers_) {
+    std::vector<apps::mojom::CapabilityAccessPtr> accesses;
+    for (const auto& item : capability_accesses) {
+      accesses.push_back(item.second->Clone());
+    }
+    subscriber->OnCapabilityAccesses(std::move(accesses));
+  }
+}
+
 void ArcApps::OnInstanceUpdate(const apps::InstanceUpdate& update) {
   if (!update.StateChanged()) {
     return;
diff --git a/chrome/browser/apps/app_service/publishers/arc_apps.h b/chrome/browser/apps/app_service/publishers/arc_apps.h
index 0fb9ee5..749244b 100644
--- a/chrome/browser/apps/app_service/publishers/arc_apps.h
+++ b/chrome/browser/apps/app_service/publishers/arc_apps.h
@@ -11,10 +11,13 @@
 #include <string>
 #include <vector>
 
+#include "ash/components/arc/mojom/app_permissions.mojom.h"
 #include "ash/components/arc/mojom/intent_helper.mojom-forward.h"
+#include "ash/components/arc/mojom/privacy_items.mojom.h"
 #include "ash/public/cpp/message_center/arc_notification_manager_base.h"
 #include "ash/public/cpp/message_center/arc_notifications_host_initializer.h"
 #include "base/callback.h"
+#include "base/containers/flat_set.h"
 #include "base/gtest_prod_util.h"
 #include "base/memory/weak_ptr.h"
 #include "base/scoped_observation.h"
@@ -63,7 +66,8 @@
                 public arc::ArcIntentHelperObserver,
                 public ash::ArcNotificationManagerBase::Observer,
                 public ash::ArcNotificationsHostInitializer::Observer,
-                public apps::InstanceRegistry::Observer {
+                public apps::InstanceRegistry::Observer,
+                public arc::mojom::PrivacyItemsHost {
  public:
   static ArcApps* Get(Profile* profile);
 
@@ -83,6 +87,7 @@
   friend class ArcAppsFactory;
   friend class PublisherTest;
   FRIEND_TEST_ALL_PREFIXES(PublisherTest, ArcAppsOnApps);
+  FRIEND_TEST_ALL_PREFIXES(PublisherTest, ArcApps_CapabilityAccess);
 
   using AppIdToTaskIds = std::map<std::string, std::set<int>>;
   using TaskIdToAppId = std::map<int, std::string>;
@@ -196,6 +201,12 @@
   void OnArcNotificationManagerDestroyed(
       ash::ArcNotificationManagerBase* notification_manager) override;
 
+  // PrivacyItemsHost overrides.
+  void OnPrivacyItemsChanged(
+      std::vector<arc::mojom::PrivacyItemPtr> privacy_items) override;
+  void OnMicCameraIndicatorRequirementChanged(bool flag) override {}
+  void OnLocationIndicatorRequirementChanged(bool flag) override {}
+
   // apps::InstanceRegistry::Observer overrides.
   void OnInstanceUpdate(const apps::InstanceUpdate& update) override;
   void OnInstanceRegistryWillBeDestroyed(
@@ -250,6 +261,9 @@
   AppIdToTaskIds app_id_to_task_ids_;
   TaskIdToAppId task_id_to_app_id_;
 
+  // App id set which might be accessing camera or microphone.
+  base::flat_set<std::string> accessing_apps_;
+
   // Handles requesting app shortcuts from Android.
   std::unique_ptr<arc::ArcAppShortcutsRequest> arc_app_shortcuts_request_;
 
diff --git a/chrome/browser/apps/app_service/publishers/publisher_unittest.cc b/chrome/browser/apps/app_service/publishers/publisher_unittest.cc
index a6d8384..12ce3cc 100644
--- a/chrome/browser/apps/app_service/publishers/publisher_unittest.cc
+++ b/chrome/browser/apps/app_service/publishers/publisher_unittest.cc
@@ -52,6 +52,8 @@
 #include "chrome/browser/ui/app_list/internal_app/internal_app_metadata.h"
 #include "chrome/common/chrome_features.h"
 #include "chromeos/login/login_state/login_state.h"
+#include "components/services/app_service/public/cpp/app_capability_access_cache.h"
+#include "components/services/app_service/public/cpp/capability_access_update.h"
 #include "components/user_manager/scoped_user_manager.h"
 #endif  // BUILDFLAG(IS_CHROMEOS_ASH)
 
@@ -181,6 +183,18 @@
   return arg.show_in_launcher.has_value() && arg.show_in_launcher == shown;
 }
 
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+arc::mojom::PrivacyItemPtr CreateArcPrivacyItem(
+    arc::mojom::AppPermissionGroup permission,
+    const std::string& package_name) {
+  arc::mojom::PrivacyItemPtr item = arc::mojom::PrivacyItem::New();
+  item->permission_group = permission;
+  item->privacy_application = arc::mojom::PrivacyApplication::New();
+  item->privacy_application->package_name = package_name;
+  return item;
+}
+#endif  // BUILDFLAG(IS_CHROMEOS_ASH)
+
 }  // namespace
 
 namespace apps {
@@ -364,6 +378,22 @@
     ASSERT_TRUE(base::Contains(cache.InitializedAppTypes(), app_type));
   }
 
+  void VerifyCapabilityAccess(const std::string& app_id,
+                              apps::mojom::OptionalBool accessing_camera,
+                              apps::mojom::OptionalBool accessing_microphone) {
+    apps::mojom::OptionalBool camera = apps::mojom::OptionalBool::kUnknown;
+    apps::mojom::OptionalBool microphone = apps::mojom::OptionalBool::kUnknown;
+    apps::AppServiceProxyFactory::GetForProfile(profile())
+        ->AppCapabilityAccessCache()
+        .ForOneApp(app_id, [&camera, &microphone](
+                               const apps::CapabilityAccessUpdate& update) {
+          camera = update.Camera();
+          microphone = update.Microphone();
+        });
+    EXPECT_EQ(camera, accessing_camera);
+    EXPECT_EQ(microphone, accessing_microphone);
+  }
+
  protected:
   base::test::ScopedFeatureList scoped_feature_list_;
 
@@ -447,6 +477,108 @@
   arc_apps->Shutdown();
 }
 
+TEST_F(PublisherTest, ArcApps_CapabilityAccess) {
+  ArcAppTest arc_test;
+  arc_test.SetUp(profile());
+  AppServiceProxyFactory::GetForProfile(profile())->FlushMojoCallsForTesting();
+  ArcApps* arc_apps = apps::ArcAppsFactory::GetForProfile(profile());
+  ASSERT_TRUE(arc_apps);
+
+  const auto& fake_apps = arc_test.fake_apps();
+  std::string package_name1 = fake_apps[0]->package_name;
+  std::string package_name2 = fake_apps[1]->package_name;
+
+  // Install fake apps.
+  arc_test.app_instance()->SendRefreshAppList(arc_test.fake_apps());
+
+  // Set accessing Camera for `package_name1`.
+  {
+    std::vector<arc::mojom::PrivacyItemPtr> privacy_items;
+    privacy_items.push_back(CreateArcPrivacyItem(
+        arc::mojom::AppPermissionGroup::CAMERA, package_name1));
+    arc_apps->OnPrivacyItemsChanged(std::move(privacy_items));
+    AppServiceProxyFactory::GetForProfile(profile())
+        ->FlushMojoCallsForTesting();
+    VerifyCapabilityAccess(
+        ArcAppTest::GetAppId(*fake_apps[0]),
+        /*accessing_camera=*/apps::mojom::OptionalBool::kTrue,
+        /*accessing_microphone=*/apps::mojom::OptionalBool::kUnknown);
+  }
+
+  // Cancel accessing Camera for `package_name1`.
+  {
+    std::vector<arc::mojom::PrivacyItemPtr> privacy_items;
+    arc_apps->OnPrivacyItemsChanged(std::move(privacy_items));
+    AppServiceProxyFactory::GetForProfile(profile())
+        ->FlushMojoCallsForTesting();
+    VerifyCapabilityAccess(
+        ArcAppTest::GetAppId(*fake_apps[0]),
+        /*accessing_camera=*/apps::mojom::OptionalBool::kFalse,
+        /*accessing_microphone=*/apps::mojom::OptionalBool::kFalse);
+  }
+
+  // Set accessing Camera and Microphone for `package_name1`, and accessing
+  // Camera for `package_name2`.
+  {
+    std::vector<arc::mojom::PrivacyItemPtr> privacy_items;
+    privacy_items.push_back(CreateArcPrivacyItem(
+        arc::mojom::AppPermissionGroup::CAMERA, package_name1));
+    privacy_items.push_back(CreateArcPrivacyItem(
+        arc::mojom::AppPermissionGroup::MICROPHONE, package_name1));
+    privacy_items.push_back(CreateArcPrivacyItem(
+        arc::mojom::AppPermissionGroup::CAMERA, package_name2));
+    arc_apps->OnPrivacyItemsChanged(std::move(privacy_items));
+    AppServiceProxyFactory::GetForProfile(profile())
+        ->FlushMojoCallsForTesting();
+    VerifyCapabilityAccess(
+        ArcAppTest::GetAppId(*fake_apps[0]),
+        /*accessing_camera=*/apps::mojom::OptionalBool::kTrue,
+        /*accessing_microphone=*/apps::mojom::OptionalBool::kTrue);
+    VerifyCapabilityAccess(
+        ArcAppTest::GetAppId(*fake_apps[1]),
+        /*accessing_camera=*/apps::mojom::OptionalBool::kTrue,
+        /*accessing_microphone=*/apps::mojom::OptionalBool::kUnknown);
+  }
+
+  // Cancel accessing Microphone for `package_name1`.
+  {
+    std::vector<arc::mojom::PrivacyItemPtr> privacy_items;
+    privacy_items.push_back(CreateArcPrivacyItem(
+        arc::mojom::AppPermissionGroup::CAMERA, package_name1));
+    privacy_items.push_back(CreateArcPrivacyItem(
+        arc::mojom::AppPermissionGroup::CAMERA, package_name2));
+    arc_apps->OnPrivacyItemsChanged(std::move(privacy_items));
+    AppServiceProxyFactory::GetForProfile(profile())
+        ->FlushMojoCallsForTesting();
+    VerifyCapabilityAccess(
+        ArcAppTest::GetAppId(*fake_apps[0]),
+        /*accessing_camera=*/apps::mojom::OptionalBool::kTrue,
+        /*accessing_microphone=*/apps::mojom::OptionalBool::kFalse);
+    VerifyCapabilityAccess(
+        ArcAppTest::GetAppId(*fake_apps[1]),
+        /*accessing_camera=*/apps::mojom::OptionalBool::kTrue,
+        /*accessing_microphone=*/apps::mojom::OptionalBool::kFalse);
+  }
+
+  // Cancel accessing CAMERA for `package_name1` and `package_name2`.
+  {
+    std::vector<arc::mojom::PrivacyItemPtr> privacy_items;
+    arc_apps->OnPrivacyItemsChanged(std::move(privacy_items));
+    AppServiceProxyFactory::GetForProfile(profile())
+        ->FlushMojoCallsForTesting();
+    VerifyCapabilityAccess(
+        ArcAppTest::GetAppId(*fake_apps[0]),
+        /*accessing_camera=*/apps::mojom::OptionalBool::kFalse,
+        /*accessing_microphone=*/apps::mojom::OptionalBool::kFalse);
+    VerifyCapabilityAccess(
+        ArcAppTest::GetAppId(*fake_apps[1]),
+        /*accessing_camera=*/apps::mojom::OptionalBool::kFalse,
+        /*accessing_microphone=*/apps::mojom::OptionalBool::kFalse);
+  }
+
+  arc_apps->Shutdown();
+}
+
 TEST_F(PublisherTest, BuiltinAppsOnApps) {
   // Verify Builtin apps are added to AppRegistryCache.
   for (const auto& internal_app : app_list::GetInternalAppList(profile())) {
diff --git a/chrome/browser/ash/eche_app/eche_app_manager_factory.cc b/chrome/browser/ash/eche_app/eche_app_manager_factory.cc
index c6975f7..2107b50 100644
--- a/chrome/browser/ash/eche_app/eche_app_manager_factory.cc
+++ b/chrome/browser/ash/eche_app/eche_app_manager_factory.cc
@@ -8,15 +8,12 @@
 
 #include "ash/components/phonehub/phone_hub_manager.h"
 #include "ash/constants/ash_features.h"
-#include "ash/root_window_controller.h"
 #include "ash/services/secure_channel/presence_monitor_impl.h"
 #include "ash/services/secure_channel/public/cpp/client/presence_monitor_client_impl.h"
 #include "ash/services/secure_channel/public/cpp/shared/presence_monitor.h"
-#include "ash/shell.h"
-#include "ash/system/eche/eche_tray.h"
-#include "ash/system/status_area_widget.h"
 #include "ash/webui/eche_app_ui/apps_access_manager_impl.h"
 #include "ash/webui/eche_app_ui/eche_app_manager.h"
+#include "ash/webui/eche_app_ui/eche_tray_stream_status_observer.h"
 #include "ash/webui/eche_app_ui/eche_uid_provider.h"
 #include "ash/webui/eche_app_ui/system_info.h"
 #include "base/bind.h"
@@ -32,7 +29,6 @@
 #include "chrome/browser/ash/profiles/profile_helper.h"
 #include "chrome/browser/ash/secure_channel/nearby_connector_factory.h"
 #include "chrome/browser/ash/secure_channel/secure_channel_client_provider.h"
-#include "chrome/browser/ash/web_applications/eche_app_info.h"
 #include "chrome/browser/profiles/profile.h"
 #include "chrome/browser/ui/browser_list.h"
 #include "chrome/browser/ui/browser_window.h"
@@ -47,8 +43,6 @@
 #include "ui/base/l10n/l10n_util.h"
 #include "ui/chromeos/devicetype_utils.h"
 #include "ui/gfx/image/image.h"
-#include "ui/views/view.h"
-#include "ui/views/widget/widget.h"
 #include "url/gurl.h"
 
 namespace ash {
@@ -56,12 +50,6 @@
 
 namespace {
 
-EcheTray* GetEcheTray() {
-  return Shell::GetPrimaryRootWindowController()
-      ->GetStatusAreaWidget()
-      ->eche_tray();
-}
-
 // Enumeration of possible interactions with a PhoneHub notification. Keep in
 // sync with corresponding enum in tools/metrics/histograms/enums.xml. These
 // values are persisted to logs. Entries should not be renumbered and numeric
@@ -72,12 +60,6 @@
   kMaxValue = kOpenAppStreaming,
 };
 
-void LaunchBubble(const GURL& url, const gfx::Image& icon) {
-  auto* eche_tray = GetEcheTray();
-  DCHECK(eche_tray);
-  eche_tray->LoadBubble(url, icon);
-}
-
 void LaunchWebApp(const std::string& package_name,
                   const absl::optional<int64_t>& notification_id,
                   const std::u16string& visible_name,
@@ -199,9 +181,7 @@
 // static
 void EcheAppManagerFactory::CloseEche(Profile* profile) {
   if (features::IsEcheCustomWidgetEnabled()) {
-    auto* eche_tray = GetEcheTray();
-    if (eche_tray)
-      eche_tray->PurgeAndClose();
+    CloseBubble();
     return;
   }
   for (auto* browser : *(BrowserList::GetInstance())) {
@@ -234,20 +214,6 @@
       ->CloseConnectionOrLaunchErrorNotifications();
 }
 
-// static
-void EcheAppManagerFactory::OnStreamStateChanged(
-    Profile* profile,
-    const mojom::StreamStatus status) {
-  if (status == mojom::StreamStatus::kStreamStatusStarted &&
-      features::IsEcheCustomWidgetEnabled()) {
-    auto* eche_tray = GetEcheTray();
-    if (eche_tray)
-      eche_tray->ShowBubble();
-  } else if (status == mojom::StreamStatus::kStreamStatusStopped) {
-    CloseEche(profile);
-  }
-}
-
 EcheAppManagerFactory::EcheAppManagerFactory()
     : BrowserContextKeyedServiceFactory(
           "EcheAppManager",
@@ -306,9 +272,7 @@
       base::BindRepeating(&EcheAppManagerFactory::LaunchEcheApp, profile),
       base::BindRepeating(&EcheAppManagerFactory::CloseEche, profile),
       base::BindRepeating(&EcheAppManagerFactory::ShowNotification,
-                          weak_ptr_factory_.GetWeakPtr(), profile),
-      base::BindRepeating(&EcheAppManagerFactory::OnStreamStateChanged,
-                          profile));
+                          weak_ptr_factory_.GetWeakPtr(), profile));
 }
 
 std::unique_ptr<SystemInfo> EcheAppManagerFactory::GetSystemInfo(
diff --git a/chrome/browser/ash/eche_app/eche_app_manager_factory.h b/chrome/browser/ash/eche_app/eche_app_manager_factory.h
index 8005689..ca36bbe5 100644
--- a/chrome/browser/ash/eche_app/eche_app_manager_factory.h
+++ b/chrome/browser/ash/eche_app/eche_app_manager_factory.h
@@ -100,8 +100,6 @@
                             const std::u16string& visible_name,
                             const absl::optional<int64_t>& user_id,
                             const gfx::Image& icon);
-  static void OnStreamStateChanged(Profile* profile,
-                                   const mojom::StreamStatus status);
 
   void SetLastLaunchedAppInfo(
       std::unique_ptr<LaunchedAppInfo> last_launched_app_info);
diff --git a/chrome/browser/ash/eche_app/eche_app_manager_factory_unittest.cc b/chrome/browser/ash/eche_app/eche_app_manager_factory_unittest.cc
index c3bb8d6..96b6b478 100644
--- a/chrome/browser/ash/eche_app/eche_app_manager_factory_unittest.cc
+++ b/chrome/browser/ash/eche_app/eche_app_manager_factory_unittest.cc
@@ -143,29 +143,6 @@
   EXPECT_FALSE(eche_tray()->is_active());
 }
 
-TEST_F(EcheAppManagerFactoryTest, OnStreamStateChanged) {
-  const int64_t user_id = 1;
-  const char16_t visible_name[] = u"Fake App";
-  const char package_name[] = "com.fakeapp";
-  EcheAppManagerFactory::LaunchEcheApp(
-      GetProfile(), /*notification_id=*/absl::nullopt, package_name,
-      visible_name, user_id, gfx::Image());
-
-  // Eche tray should be visible when streaming is active
-  EcheAppManagerFactory::OnStreamStateChanged(
-      GetProfile(), mojom::StreamStatus::kStreamStatusStarted);
-  // Wait for Eche Tray to load Eche Web to complete
-  base::RunLoop().RunUntilIdle();
-  EXPECT_TRUE(eche_tray()->is_active());
-
-  // Eche tray should not be visible when streaming is finished
-  EcheAppManagerFactory::OnStreamStateChanged(
-      GetProfile(), mojom::StreamStatus::kStreamStatusStopped);
-  // Wait for Eche Web to close
-  base::RunLoop().RunUntilIdle();
-  EXPECT_FALSE(eche_tray()->is_active());
-}
-
 TEST_F(EcheAppManagerFactoryWithBackgroundTest, LaunchEcheApp) {
   const int64_t user_id = 1;
   const char16_t visible_name[] = u"Fake App";
@@ -180,28 +157,5 @@
   EXPECT_FALSE(eche_tray()->is_active());
 }
 
-TEST_F(EcheAppManagerFactoryWithBackgroundTest, OnStreamStateChanged) {
-  const int64_t user_id = 1;
-  const char16_t visible_name[] = u"Fake App";
-  const char package_name[] = "com.fakeapp";
-  EcheAppManagerFactory::LaunchEcheApp(
-      GetProfile(), /*notification_id=*/absl::nullopt, package_name,
-      visible_name, user_id, gfx::Image());
-
-  // Eche tray should be visible when streaming is active
-  EcheAppManagerFactory::OnStreamStateChanged(
-      GetProfile(), mojom::StreamStatus::kStreamStatusStarted);
-  // Wait for Eche Tray to load Eche Web to complete
-  base::RunLoop().RunUntilIdle();
-  EXPECT_TRUE(eche_tray()->is_active());
-
-  // Eche tray should not be visible when streaming is finished
-  EcheAppManagerFactory::OnStreamStateChanged(
-      GetProfile(), mojom::StreamStatus::kStreamStatusStopped);
-  // Wait for Eche Web to close
-  base::RunLoop().RunUntilIdle();
-  EXPECT_FALSE(eche_tray()->is_active());
-}
-
 }  // namespace eche_app
 }  // namespace ash
diff --git a/chrome/browser/chrome_browser_interface_binders.cc b/chrome/browser/chrome_browser_interface_binders.cc
index f4650bd8..1b56197 100644
--- a/chrome/browser/chrome_browser_interface_binders.cc
+++ b/chrome/browser/chrome_browser_interface_binders.cc
@@ -16,6 +16,7 @@
 #include "chrome/browser/dom_distiller/dom_distiller_service_factory.h"
 #include "chrome/browser/media/history/media_history_store.mojom.h"
 #include "chrome/browser/media/media_engagement_score_details.mojom.h"
+#include "chrome/browser/navigation_predictor/anchor_element_preloader.h"
 #include "chrome/browser/navigation_predictor/navigation_predictor.h"
 #include "chrome/browser/password_manager/chrome_password_manager_client.h"
 #include "chrome/browser/predictors/network_hints_handler_impl.h"
@@ -78,6 +79,7 @@
 #include "services/image_annotation/public/mojom/image_annotation.mojom.h"
 #include "third_party/blink/public/common/features.h"
 #include "third_party/blink/public/mojom/credentialmanagement/credential_manager.mojom.h"
+#include "third_party/blink/public/mojom/loader/anchor_element_interaction_host.mojom.h"
 #include "third_party/blink/public/mojom/loader/navigation_predictor.mojom.h"
 #include "third_party/blink/public/mojom/payments/payment_credential.mojom.h"
 #include "third_party/blink/public/mojom/payments/payment_request.mojom.h"
@@ -629,6 +631,12 @@
   map->Add<blink::mojom::AnchorElementMetricsHost>(
       base::BindRepeating(&NavigationPredictor::Create));
 
+  if (base::FeatureList::IsEnabled(
+          blink::features::kAnchorElementInteraction)) {
+    map->Add<blink::mojom::AnchorElementInteractionHost>(
+        base::BindRepeating(&AnchorElementPreloader::Create));
+  }
+
   map->Add<dom_distiller::mojom::DistillabilityService>(
       base::BindRepeating(&BindDistillabilityService));
 
diff --git a/chrome/browser/chrome_content_browser_client.cc b/chrome/browser/chrome_content_browser_client.cc
index 40b7f4772..61a8155 100644
--- a/chrome/browser/chrome_content_browser_client.cc
+++ b/chrome/browser/chrome_content_browser_client.cc
@@ -4053,6 +4053,13 @@
       break;
   }
 
+#if !defined(OFFICIAL_BUILD)
+  // Disable renderer code integrity when Application Verifier or pageheap are
+  // enabled for chrome.exe to avoid renderer crashes. https://crbug.com/1004989
+  if (base::win::IsAppVerifierEnabled(chrome::kBrowserProcessExecutableName))
+    enforce_code_integrity = false;
+#endif  // !defined(OFFICIAL_BUILD)
+
   if (!enforce_code_integrity)
     return true;
 
diff --git a/chrome/browser/extensions/api/downloads/downloads_api.cc b/chrome/browser/extensions/api/downloads/downloads_api.cc
index d26484d..23a53f0 100644
--- a/chrome/browser/extensions/api/downloads/downloads_api.cc
+++ b/chrome/browser/extensions/api/downloads/downloads_api.cc
@@ -916,8 +916,9 @@
     content::BrowserContext* browser_context,
     Feature::Context target_context,
     const Extension* extension,
-    Event* event,
-    const base::DictionaryValue* listener_filter) {
+    const base::DictionaryValue* listener_filter,
+    std::unique_ptr<base::Value::List>* event_args_out,
+    mojom::EventFilteringInfoPtr* event_filtering_info_out) {
   *any_determiners = true;
   base::Time installed =
       ExtensionPrefs::Get(browser_context)->GetInstallTime(extension->id());
diff --git a/chrome/browser/extensions/api/tabs/tabs_apitest.cc b/chrome/browser/extensions/api/tabs/tabs_apitest.cc
index 9f0e3b9..2d28188 100644
--- a/chrome/browser/extensions/api/tabs/tabs_apitest.cc
+++ b/chrome/browser/extensions/api/tabs/tabs_apitest.cc
@@ -17,9 +17,13 @@
 #include "chrome/common/chrome_switches.h"
 #include "chrome/common/pref_names.h"
 #include "chrome/common/url_constants.h"
+#include "chrome/test/base/ui_test_utils.h"
 #include "components/prefs/pref_service.h"
 #include "content/public/common/content_features.h"
 #include "content/public/test/browser_test.h"
+#include "content/public/test/browser_test_utils.h"
+#include "extensions/test/result_catcher.h"
+#include "extensions/test/test_extension_dir.h"
 #include "net/dns/mock_host_resolver.h"
 
 #if BUILDFLAG(IS_WIN)
@@ -379,6 +383,67 @@
   ASSERT_TRUE(RunExtensionTest("tabs/send_message"));
 }
 
+// Tests that extension with "tabs" permission does not leak tab info to another
+// extension without "tabs" permission.
+//
+// Regression test for https://crbug.com/1302959
+IN_PROC_BROWSER_TEST_F(ExtensionApiTabTest, TabsPermissionDoesNotLeakTabInfo) {
+  constexpr char kManifestWithTabsPermission[] =
+      R"({
+        "name": "test", "version": "1", "manifest_version": 2,
+        "background": {"scripts": ["background.js"]},
+        "permissions": ["tabs"]
+      })";
+  constexpr char kBackgroundJSWithTabsPermission[] =
+      "chrome.tabs.onUpdated.addListener(() => {});";
+
+  constexpr char kManifestWithoutTabsPermission[] =
+      R"({
+        "name": "test", "version": "1", "manifest_version": 2,
+        "background": {"scripts": ["background.js"]}
+      })";
+  constexpr char kBackgroundJSWithoutTabsPermission[] =
+      R"(
+        let urlStr = '%s';
+        chrome.tabs.onUpdated.addListener(function(tabId, changeInfo, tab) {
+          chrome.test.assertEq(3, Array.from(arguments).length);
+          // Note: we'll search within all of the arguments, just to make sure
+          // we don't miss any inadvertently added ones. See
+          // https://crbug.com/1302959 for details.
+          let argumentsStr = JSON.stringify(arguments);
+          let containsUrlStr = argumentsStr.indexOf(urlStr) != -1;
+          chrome.test.assertFalse(containsUrlStr);
+          if (tab.status == 'complete') {
+            chrome.test.notifyPass();
+          }
+        });
+      )";
+
+  GURL url = embedded_test_server()->GetURL("/title1.html");
+
+  // First load the extension with "tabs" permission.
+  // Note that order is important for this regression test.
+  extensions::TestExtensionDir ext_dir1;
+  ext_dir1.WriteManifest(kManifestWithTabsPermission);
+  ext_dir1.WriteFile(FILE_PATH_LITERAL("background.js"),
+                     kBackgroundJSWithTabsPermission);
+  ASSERT_TRUE(LoadExtension(ext_dir1.UnpackedPath()));
+
+  // Then load the extension without "tabs" permission.
+  extensions::ResultCatcher catcher;
+  extensions::TestExtensionDir ext_dir2;
+  ext_dir2.WriteManifest(kManifestWithoutTabsPermission);
+  ext_dir2.WriteFile(FILE_PATH_LITERAL("background.js"),
+                     base::StringPrintf(kBackgroundJSWithoutTabsPermission,
+                                        url.spec().c_str()));
+  ASSERT_TRUE(LoadExtension(ext_dir2.UnpackedPath()));
+
+  // Now open a tab and ensure the extension in |ext_dir2| does not see any info
+  // that is guarded by "tabs" permission.
+  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));
+  EXPECT_TRUE(catcher.GetNextResult()) << catcher.message();
+}
+
 class IncognitoExtensionApiTabTest : public ExtensionApiTabTest,
                                      public testing::WithParamInterface<bool> {
 };
diff --git a/chrome/browser/extensions/api/tabs/tabs_event_router.cc b/chrome/browser/extensions/api/tabs/tabs_event_router.cc
index e280973..18c3467 100644
--- a/chrome/browser/extensions/api/tabs/tabs_event_router.cc
+++ b/chrome/browser/extensions/api/tabs/tabs_event_router.cc
@@ -47,8 +47,9 @@
     content::BrowserContext* browser_context,
     Feature::Context target_context,
     const Extension* extension,
-    Event* event,
-    const base::DictionaryValue* listener_filter) {
+    const base::DictionaryValue* listener_filter,
+    std::unique_ptr<base::Value::List>* event_args_out,
+    mojom::EventFilteringInfoPtr* event_filtering_info_out) {
   ExtensionTabUtil::ScrubTabBehavior scrub_tab_behavior =
       ExtensionTabUtil::GetScrubTabBehavior(extension, target_context,
                                             contents);
@@ -65,19 +66,22 @@
       changed_properties.Set(property, value->Clone());
   }
 
-  event->event_args->Append(base::Value(std::move(changed_properties)));
-  event->event_args->Append(std::move(tab_value));
+  *event_args_out = std::make_unique<base::Value::List>();
+  (*event_args_out)->Append(ExtensionTabUtil::GetTabId(contents));
+  (*event_args_out)->Append(base::Value(std::move(changed_properties)));
+  (*event_args_out)->Append(std::move(tab_value));
   return true;
 }
 
-bool WillDispatchTabCreatedEvent(WebContents* contents,
-                                 bool active,
-                                 content::BrowserContext* browser_context,
-                                 Feature::Context target_context,
-                                 const Extension* extension,
-                                 Event* event,
-                                 const base::DictionaryValue* listener_filter) {
-  event->event_args->ClearList();
+bool WillDispatchTabCreatedEvent(
+    WebContents* contents,
+    bool active,
+    content::BrowserContext* browser_context,
+    Feature::Context target_context,
+    const Extension* extension,
+    const base::DictionaryValue* listener_filter,
+    std::unique_ptr<base::Value::List>* event_args_out,
+    mojom::EventFilteringInfoPtr* event_filtering_info_out) {
   ExtensionTabUtil::ScrubTabBehavior scrub_tab_behavior =
       ExtensionTabUtil::GetScrubTabBehavior(extension, target_context,
                                             contents);
@@ -86,7 +90,9 @@
           ->ToValue());
   tab_value.SetBoolKey(tabs_constants::kSelectedKey, active);
   tab_value.SetBoolKey(tabs_constants::kActiveKey, active);
-  event->event_args->Append(std::move(tab_value));
+
+  *event_args_out = std::make_unique<base::Value::List>();
+  (*event_args_out)->Append(std::move(tab_value));
   return true;
 }
 
@@ -585,24 +591,13 @@
   DCHECK(!changed_property_names.empty());
   DCHECK(contents);
 
-  // The state of the tab (as seen from the extension point of view) has
-  // changed.  Send a notification to the extension.
-  std::unique_ptr<base::ListValue> args_base(new base::ListValue);
-
-  // First arg: The id of the tab that changed.
-  args_base->Append(ExtensionTabUtil::GetTabId(contents));
-
-  // Second arg: An object containing the changes to the tab state.  Filled in
-  // by WillDispatchTabUpdatedEvent as a copy of changed_properties, if the
-  // extension has the tabs permission.
-
-  // Third arg: An object containing the state of the tab. Filled in by
-  // WillDispatchTabUpdatedEvent.
   Profile* profile = Profile::FromBrowserContext(contents->GetBrowserContext());
 
   auto event = std::make_unique<Event>(
       events::TABS_ON_UPDATED, api::tabs::OnUpdated::kEventName,
-      std::move(*args_base).TakeListDeprecated(), profile);
+      // The event arguments depend on the extension's permission. They are set
+      // in WillDispatchTabUpdatedEvent().
+      std::vector<base::Value>(), profile);
   event->user_gesture = EventRouter::USER_GESTURE_NOT_ENABLED;
   event->will_dispatch_callback =
       base::BindRepeating(&WillDispatchTabUpdatedEvent, contents,
diff --git a/chrome/browser/extensions/api/tabs/windows_event_router.cc b/chrome/browser/extensions/api/tabs/windows_event_router.cc
index bb02fdd..32e03171 100644
--- a/chrome/browser/extensions/api/tabs/windows_event_router.cc
+++ b/chrome/browser/extensions/api/tabs/windows_event_router.cc
@@ -59,12 +59,14 @@
              WindowController::GetFilterFromWindowTypesValues(filter_value));
 }
 
-bool WillDispatchWindowEvent(WindowController* window_controller,
-                             BrowserContext* browser_context,
-                             Feature::Context target_context,
-                             const Extension* extension,
-                             Event* event,
-                             const base::DictionaryValue* listener_filter) {
+bool WillDispatchWindowEvent(
+    WindowController* window_controller,
+    BrowserContext* browser_context,
+    Feature::Context target_context,
+    const Extension* extension,
+    const base::DictionaryValue* listener_filter,
+    std::unique_ptr<base::Value::List>* event_args_out,
+    mojom::EventFilteringInfoPtr* event_filtering_info_out) {
   bool has_filter =
       listener_filter &&
       listener_filter->FindKey(extensions::tabs_constants::kWindowTypesKey);
@@ -75,15 +77,15 @@
     return false;
   }
 
-  // Cleanup previous values.
-  event->filter_info = mojom::EventFilteringInfo::New();
+  *event_filtering_info_out = mojom::EventFilteringInfo::New();
   // Only set the window type if the listener has set a filter.
   // Otherwise we set the window visibility relative to the extension.
   if (has_filter) {
-    event->filter_info->window_type = window_controller->GetWindowTypeText();
+    (*event_filtering_info_out)->window_type =
+        window_controller->GetWindowTypeText();
   } else {
-    event->filter_info->has_window_exposed_by_default = true;
-    event->filter_info->window_exposed_by_default = true;
+    (*event_filtering_info_out)->has_window_exposed_by_default = true;
+    (*event_filtering_info_out)->window_exposed_by_default = true;
   }
   return true;
 }
@@ -93,8 +95,9 @@
     BrowserContext* browser_context,
     Feature::Context target_context,
     const Extension* extension,
-    Event* event,
-    const base::DictionaryValue* listener_filter) {
+    const base::DictionaryValue* listener_filter,
+    std::unique_ptr<base::Value::List>* event_args_out,
+    mojom::EventFilteringInfoPtr* event_filtering_info_out) {
   int window_id = extension_misc::kUnknownWindowId;
   Profile* new_active_context = nullptr;
   bool has_filter =
@@ -108,19 +111,18 @@
     new_active_context = window_controller->profile();
   }
 
-  // Cleanup previous values.
-  event->filter_info = mojom::EventFilteringInfo::New();
+  *event_filtering_info_out = mojom::EventFilteringInfo::New();
   // Only set the window type if the listener has set a filter,
   // otherwise set the visibility to true (if the window is not
   // supposed to be visible by the extension, we will clear out the
   // window id later).
   if (has_filter) {
-    event->filter_info->window_type =
+    (*event_filtering_info_out)->window_type =
         window_controller ? window_controller->GetWindowTypeText()
                           : extensions::tabs_constants::kWindowTypeValueNormal;
   } else {
-    event->filter_info->has_window_exposed_by_default = true;
-    event->filter_info->window_exposed_by_default = true;
+    (*event_filtering_info_out)->has_window_exposed_by_default = true;
+    (*event_filtering_info_out)->window_exposed_by_default = true;
   }
 
   // When switching between windows in the default and incognito profiles,
@@ -135,12 +137,11 @@
   bool visible_to_listener = ControllerVisibleToListener(
       window_controller, extension, listener_filter);
 
+  *event_args_out = std::make_unique<base::Value::List>();
   if (cant_cross_incognito || !visible_to_listener) {
-    event->event_args->ClearList();
-    event->event_args->Append(extension_misc::kUnknownWindowId);
+    (*event_args_out)->Append(extension_misc::kUnknownWindowId);
   } else {
-    event->event_args->ClearList();
-    event->event_args->Append(window_id);
+    (*event_args_out)->Append(window_id);
   }
   return true;
 }
diff --git a/chrome/browser/feed/android/java/src/org/chromium/chrome/browser/feed/webfeed/WebFeedBridge.java b/chrome/browser/feed/android/java/src/org/chromium/chrome/browser/feed/webfeed/WebFeedBridge.java
index eba9847..e7f1df5 100644
--- a/chrome/browser/feed/android/java/src/org/chromium/chrome/browser/feed/webfeed/WebFeedBridge.java
+++ b/chrome/browser/feed/android/java/src/org/chromium/chrome/browser/feed/webfeed/WebFeedBridge.java
@@ -141,6 +141,11 @@
         WebFeedBridgeJni.get().refreshRecommendedFeeds(callback);
     }
 
+    /** Increase the count of the number of times the user has followed from the web page menu. */
+    public static void incrementFollowedFromWebPageMenuCount() {
+        WebFeedBridgeJni.get().incrementFollowedFromWebPageMenuCount();
+    }
+
     /** Container for results from a follow request. */
     public static class FollowResults {
         /** Status of follow request. */
@@ -249,5 +254,6 @@
         void refreshSubscriptions(Callback<Boolean> callback);
         void refreshRecommendedFeeds(Callback<Boolean> callback);
         void getRecentVisitCountsToHost(GURL url, Callback<int[]> callback);
+        void incrementFollowedFromWebPageMenuCount();
     }
 }
diff --git a/chrome/browser/feed/android/java/src/org/chromium/chrome/browser/feed/webfeed/WebFeedMainMenuItem.java b/chrome/browser/feed/android/java/src/org/chromium/chrome/browser/feed/webfeed/WebFeedMainMenuItem.java
index 01645cf..2993db0 100644
--- a/chrome/browser/feed/android/java/src/org/chromium/chrome/browser/feed/webfeed/WebFeedMainMenuItem.java
+++ b/chrome/browser/feed/android/java/src/org/chromium/chrome/browser/feed/webfeed/WebFeedMainMenuItem.java
@@ -17,8 +17,11 @@
 
 import org.chromium.base.Callback;
 import org.chromium.chrome.browser.feed.FeedFeatures;
+import org.chromium.chrome.browser.feed.FeedServiceBridge;
 import org.chromium.chrome.browser.feed.R;
+import org.chromium.chrome.browser.feed.StreamKind;
 import org.chromium.chrome.browser.feed.componentinterfaces.SurfaceCoordinator.StreamTabId;
+import org.chromium.chrome.browser.feed.v2.FeedUserActionType;
 import org.chromium.chrome.browser.feed.webfeed.WebFeedBridge.WebFeedMetadata;
 import org.chromium.chrome.browser.feed.webfeed.WebFeedSnackbarController.FeedLauncher;
 import org.chromium.chrome.browser.preferences.Pref;
@@ -152,6 +155,9 @@
                             FeedFeatures.setLastSeenFeedTabId(StreamTabId.FOLLOWING);
                         }
                     });
+                    WebFeedBridge.incrementFollowedFromWebPageMenuCount();
+                    FeedServiceBridge.reportOtherUserAction(
+                            StreamKind.UNKNOWN, FeedUserActionType.TAPPED_FOLLOW_BUTTON);
                     mAppMenuHandler.hideAppMenu();
                 });
     }
diff --git a/chrome/browser/feed/android/web_feed_bridge.cc b/chrome/browser/feed/android/web_feed_bridge.cc
index cface92..29d5178a 100644
--- a/chrome/browser/feed/android/web_feed_bridge.cc
+++ b/chrome/browser/feed/android/web_feed_bridge.cc
@@ -14,6 +14,7 @@
 #include "base/notreached.h"
 #include "base/task/cancelable_task_tracker.h"
 #include "chrome/browser/android/tab_android.h"
+#include "chrome/browser/feed/android/feed_stream.h"
 #include "chrome/browser/feed/android/jni_headers/WebFeedBridge_jni.h"
 #include "chrome/browser/feed/feed_service_factory.h"
 #include "chrome/browser/feed/web_feed_follow_util.h"
@@ -32,6 +33,8 @@
 #include "third_party/abseil-cpp/absl/types/optional.h"
 #include "url/android/gurl_android.h"
 
+class Profile;
+
 namespace feed {
 
 using PageInformation = WebFeedPageInformationFetcher::PageInformation;
@@ -79,6 +82,14 @@
   return GetSubscriptionsForProfile(profile);
 }
 
+FeedApi* GetStream() {
+  Profile* profile = ProfileManager::GetLastUsedProfile();
+  FeedService* service = FeedServiceFactory::GetForBrowserContext(profile);
+  if (!service)
+    return nullptr;
+  return service->GetStream();
+}
+
 // ToJava functions convert C++ types to Java. Used in `AdaptCallbackForJava`.
 
 bool ToJava(JNIEnv* env, WebFeedSubscriptions::RefreshResult value) {
@@ -347,4 +358,13 @@
       std::move(callback), &TaskTracker());
 }
 
+static void JNI_WebFeedBridge_IncrementFollowedFromWebPageMenuCount(
+    JNIEnv* env) {
+  FeedApi* stream = GetStream();
+  if (!stream)
+    return;
+
+  stream->IncrementFollowedFromWebPageMenuCount();
+}
+
 }  // namespace feed
diff --git a/chrome/browser/flag-metadata.json b/chrome/browser/flag-metadata.json
index 4c32bcb..518f07e 100644
--- a/chrome/browser/flag-metadata.json
+++ b/chrome/browser/flag-metadata.json
@@ -284,6 +284,11 @@
     "expiry_milestone": 110
   },
   {
+    "name": "ash-capture-mode-selfie-cam",
+    "owners": [ "afakhry", "gzadina" ],
+    "expiry_milestone": 112
+  },
+  {
     "name": "ash-debug-shortcuts",
     "owners": [ "//ash/OWNERS" ],
     // Used by developers for debugging and to dump extra information to logs
@@ -410,11 +415,6 @@
     "expiry_milestone": 103
   },
   {
-    "name": "autofill-enable-google-issued-card",
-    "owners": [ "siashah" ],
-    "expiry_milestone": 92
-  },
-  {
     "name": "autofill-enable-merchant-bound-virtual-cards",
     "owners": [ "siashah", "siyua" ],
     "expiry_milestone": 105
@@ -4188,11 +4188,6 @@
     "expiry_milestone": 105
   },
   {
-    "name": "new-usb-backend",
-    "owners": [ "reillyg@chromium.org" ],
-    "expiry_milestone": 100
-  },
-  {
     "name": "new-window-app-menu",
     "owners": [ "jinsukkim" ],
     "expiry_milestone": 100
diff --git a/chrome/browser/flag_descriptions.cc b/chrome/browser/flag_descriptions.cc
index d2bd35e..98ff2f8 100644
--- a/chrome/browser/flag_descriptions.cc
+++ b/chrome/browser/flag_descriptions.cc
@@ -318,12 +318,6 @@
     "Controls if different width limits are used for the popup that provides "
     "Autofill suggestions, depending on the type of data that is filled.";
 
-const char kAutofillEnableGoogleIssuedCardName[] =
-    "Enable Autofill Google-issued card";
-const char kAutofillEnableGoogleIssuedCardDescription[] =
-    "When enabled, Google-issued cards will be available in the autofill "
-    "suggestions.";
-
 const char kAutofillEnableMerchantBoundVirtualCardsName[] =
     "Offer merchant bound virtual cards in Autofill";
 const char kAutofillEnableMerchantBoundVirtualCardsDescription[] =
@@ -1663,10 +1657,6 @@
     "owned by the System Profile. This requires "
     "#destroy-profile-on-browser-close.";
 
-const char kNewUsbBackendName[] = "Enable new USB backend";
-const char kNewUsbBackendDescription[] =
-    "Enables the new experimental USB backend for macOS";
-
 const char kNotificationsRevampName[] = "Notifications Revamp";
 const char kNotificationsRevampDescription[] =
     "Enable notification UI revamp and grouped web notifications.";
@@ -4249,6 +4239,11 @@
     "Show Monthly Calendar View with Google Calendar events to increase "
     "productivity by helping users view their schedules more quickly.";
 
+const char kCaptureSelfieCamName[] = "Enable selfie camera in screen capture";
+const char kCaptureSelfieCamDescription[] =
+    "Enables the ability to record the selected camera feed along with screen "
+    "recordings for personalized demos and more.";
+
 const char kDefaultLinkCapturingInBrowserName[] =
     "Default link capturing in the browser";
 const char kDefaultLinkCapturingInBrowserDescription[] =
diff --git a/chrome/browser/flag_descriptions.h b/chrome/browser/flag_descriptions.h
index cd2f531..9e0fce4 100644
--- a/chrome/browser/flag_descriptions.h
+++ b/chrome/browser/flag_descriptions.h
@@ -186,9 +186,6 @@
 extern const char kAutofillTypeSpecificPopupWidthName[];
 extern const char kAutofillTypeSpecificPopupWidthDescription[];
 
-extern const char kAutofillEnableGoogleIssuedCardName[];
-extern const char kAutofillEnableGoogleIssuedCardDescription[];
-
 extern const char kAutofillEnableMerchantBoundVirtualCardsName[];
 extern const char kAutofillEnableMerchantBoundVirtualCardsDescription[];
 
@@ -947,9 +944,6 @@
 extern const char kDestroySystemProfilesName[];
 extern const char kDestroySystemProfilesDescription[];
 
-extern const char kNewUsbBackendName[];
-extern const char kNewUsbBackendDescription[];
-
 extern const char kNotificationsRevampName[];
 extern const char kNotificationsRevampDescription[];
 
@@ -2440,6 +2434,9 @@
 extern const char kCalendarViewName[];
 extern const char kCalendarViewDescription[];
 
+extern const char kCaptureSelfieCamName[];
+extern const char kCaptureSelfieCamDescription[];
+
 extern const char kDefaultLinkCapturingInBrowserName[];
 extern const char kDefaultLinkCapturingInBrowserDescription[];
 
diff --git a/chrome/browser/flags/android/chrome_feature_list.cc b/chrome/browser/flags/android/chrome_feature_list.cc
index e4a56d58..085765a4 100644
--- a/chrome/browser/flags/android/chrome_feature_list.cc
+++ b/chrome/browser/flags/android/chrome_feature_list.cc
@@ -86,7 +86,6 @@
     &autofill::features::kAutofillKeyboardAccessory,
     &autofill::features::kAutofillManualFallbackAndroid,
     &autofill::features::kAutofillRefreshStyleAndroid,
-    &autofill::features::kAutofillEnableGoogleIssuedCard,
     &autofill::features::kAutofillEnableSupportForHonorificPrefixes,
     &autofill::features::kAutofillEnableSupportForMoreStructureInAddresses,
     &autofill::features::kAutofillEnableSupportForMoreStructureInNames,
diff --git a/chrome/browser/flags/android/java/src/org/chromium/chrome/browser/flags/ChromeFeatureList.java b/chrome/browser/flags/android/java/src/org/chromium/chrome/browser/flags/ChromeFeatureList.java
index c0730d4..3018d2e 100644
--- a/chrome/browser/flags/android/java/src/org/chromium/chrome/browser/flags/ChromeFeatureList.java
+++ b/chrome/browser/flags/android/java/src/org/chromium/chrome/browser/flags/ChromeFeatureList.java
@@ -206,8 +206,6 @@
             "AutofillAllowNonHttpActivation";
     public static final String AUTOFILL_CREDIT_CARD_AUTHENTICATION =
             "AutofillCreditCardAuthentication";
-    public static final String AUTOFILL_ENABLE_GOOGLE_ISSUED_CARD =
-            "AutofillEnableGoogleIssuedCard";
     public static final String AUTOFILL_ENABLE_SUPPORT_FOR_HONORIFIC_PREFIXES =
             "AutofillEnableSupportForHonorificPrefixes";
     public static final String AUTOFILL_ENABLE_SUPPORT_FOR_MORE_STRUCTURE_IN_ADDRESSES =
diff --git a/chrome/browser/history_clusters/BUILD.gn b/chrome/browser/history_clusters/BUILD.gn
index b784c579..1ebe397 100644
--- a/chrome/browser/history_clusters/BUILD.gn
+++ b/chrome/browser/history_clusters/BUILD.gn
@@ -11,6 +11,10 @@
     "java/src/org/chromium/chrome/browser/history_clusters/ClusterVisit.java",
     "java/src/org/chromium/chrome/browser/history_clusters/HistoryCluster.java",
     "java/src/org/chromium/chrome/browser/history_clusters/HistoryClustersBridge.java",
+    "java/src/org/chromium/chrome/browser/history_clusters/HistoryClustersCoordinator.java",
+    "java/src/org/chromium/chrome/browser/history_clusters/HistoryClustersMediator.java",
+    "java/src/org/chromium/chrome/browser/history_clusters/HistoryClustersProvider.java",
+    "java/src/org/chromium/chrome/browser/history_clusters/HistoryClustersQueryManager.java",
     "java/src/org/chromium/chrome/browser/history_clusters/HistoryClustersResult.java",
     "java/src/org/chromium/chrome/browser/history_clusters/HistoryClustersTabHelper.java",
   ]
@@ -18,8 +22,13 @@
   deps = [
     "//base:base_java",
     "//chrome/browser/profiles/android:java",
+    "//chrome/browser/ui/android/favicon:java",
+    "//components/browser_ui/widget/android:java",
+    "//components/favicon/android:java",
     "//content/public/android:content_java",
     "//third_party/androidx:androidx_annotation_annotation_java",
+    "//ui/android:ui_no_recycler_view_java",
+    "//ui/android:ui_recycler_view_java",
     "//url:gurl_java",
   ]
 
diff --git a/chrome/browser/history_clusters/DEPS b/chrome/browser/history_clusters/DEPS
index c482170..c13d4525 100644
--- a/chrome/browser/history_clusters/DEPS
+++ b/chrome/browser/history_clusters/DEPS
@@ -1,3 +1,4 @@
 include_rules = [
+  "+components/favicon/android",
   "+content/public/android",
 ]
\ No newline at end of file
diff --git a/chrome/browser/history_clusters/java/src/org/chromium/chrome/browser/history_clusters/HistoryClustersBridge.java b/chrome/browser/history_clusters/java/src/org/chromium/chrome/browser/history_clusters/HistoryClustersBridge.java
index 8b5284e..9b701e0 100644
--- a/chrome/browser/history_clusters/java/src/org/chromium/chrome/browser/history_clusters/HistoryClustersBridge.java
+++ b/chrome/browser/history_clusters/java/src/org/chromium/chrome/browser/history_clusters/HistoryClustersBridge.java
@@ -16,14 +16,18 @@
 
 @JNINamespace("history_clusters")
 /** JNI bridge that provides access to HistoryClusters data. */
-public class HistoryClustersBridge {
+class HistoryClustersBridge {
     private long mNativeBridge;
 
     /* Construct a new HistoryClustersBridge. */
-    public HistoryClustersBridge(Profile profile) {
+    HistoryClustersBridge(Profile profile) {
         mNativeBridge = HistoryClustersBridgeJni.get().init(profile);
     }
 
+    void destroy() {
+        HistoryClustersBridgeJni.get().destroy(mNativeBridge);
+    }
+
     /* Start a new query for clusters, fetching the first page of results. */
     void queryClusters(String query, Callback<HistoryClustersResult> callback) {
         HistoryClustersBridgeJni.get().queryClusters(mNativeBridge, this, query, callback);
@@ -60,5 +64,6 @@
                 String query, Callback<HistoryClustersResult> callback);
         void loadMoreClusters(long nativeHistoryClustersBridge, HistoryClustersBridge caller,
                 String query, Callback<HistoryClustersResult> callback);
+        void destroy(long nativeHistoryClustersBridge);
     }
 }
diff --git a/chrome/browser/history_clusters/java/src/org/chromium/chrome/browser/history_clusters/HistoryClustersCoordinator.java b/chrome/browser/history_clusters/java/src/org/chromium/chrome/browser/history_clusters/HistoryClustersCoordinator.java
new file mode 100644
index 0000000..ef912f10
--- /dev/null
+++ b/chrome/browser/history_clusters/java/src/org/chromium/chrome/browser/history_clusters/HistoryClustersCoordinator.java
@@ -0,0 +1,42 @@
+// 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.history_clusters;
+
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+
+import org.chromium.chrome.browser.profiles.Profile;
+import org.chromium.ui.modelutil.MVCListAdapter.ModelList;
+
+/**
+ * Root component for the HistoryClusters UI component, which displays lists of related history
+ * visits grouped into clusters.
+ */
+public class HistoryClustersCoordinator {
+    private final HistoryClustersQueryManager mHistoryClustersQueryManager;
+    private final HistoryClustersMediator mMediator;
+    private final ModelList mModelList;
+    private final Context mContext;
+
+    /**
+     * Construct a new HistoryClustersCoordinator.
+     * @param profile The profile from which the coordinator should access history data.
+     * @param context Android context from which UI configuration (strings, colors etc.) should be
+     *         derived.
+     */
+    public HistoryClustersCoordinator(@NonNull Profile profile, @NonNull Context context) {
+        mContext = context;
+        mHistoryClustersQueryManager = new HistoryClustersQueryManager(profile);
+        mModelList = new ModelList();
+        mMediator = new HistoryClustersMediator(
+                mHistoryClustersQueryManager, context, context.getResources(), mModelList, profile);
+    }
+
+    public void destroy() {
+        mHistoryClustersQueryManager.destroy();
+        mMediator.destroy();
+    }
+}
\ No newline at end of file
diff --git a/chrome/browser/history_clusters/java/src/org/chromium/chrome/browser/history_clusters/HistoryClustersMediator.java b/chrome/browser/history_clusters/java/src/org/chromium/chrome/browser/history_clusters/HistoryClustersMediator.java
new file mode 100644
index 0000000..27758765
--- /dev/null
+++ b/chrome/browser/history_clusters/java/src/org/chromium/chrome/browser/history_clusters/HistoryClustersMediator.java
@@ -0,0 +1,50 @@
+// 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.history_clusters;
+
+import android.content.Context;
+import android.content.res.Resources;
+
+import androidx.annotation.NonNull;
+
+import org.chromium.base.Promise;
+import org.chromium.chrome.browser.profiles.Profile;
+import org.chromium.chrome.browser.ui.favicon.FaviconUtils;
+import org.chromium.components.browser_ui.widget.RoundedIconGenerator;
+import org.chromium.components.favicon.LargeIconBridge;
+import org.chromium.ui.modelutil.MVCListAdapter.ModelList;
+
+class HistoryClustersMediator {
+    private final HistoryClustersProvider mHistoryClustersProvider;
+    private final Context mContext;
+    private final Resources mResources;
+    private final ModelList mModelList;
+    private final RoundedIconGenerator mIconGenerator;
+    private final LargeIconBridge mLargeIconBridge;
+    private Promise<HistoryClustersResult> mPromise;
+
+    /**
+     * Create a new HistoryClustersMediator.
+     * @param historyClustersProvider Provider of history clusters data.
+     * @param context Android context from which UI configuration should be derived.
+     * @param resources Android resources object from which strings, colors etc. should be fetched.
+     * @param modelList Model list to which fetched cluster data should be pushed to.
+     * @param profile Profile from which we should access history/favicon data.
+     */
+    HistoryClustersMediator(@NonNull HistoryClustersProvider historyClustersProvider,
+            @NonNull Context context, @NonNull Resources resources, @NonNull ModelList modelList,
+            @NonNull Profile profile) {
+        mHistoryClustersProvider = historyClustersProvider;
+        mModelList = modelList;
+        mContext = context;
+        mResources = resources;
+        mIconGenerator = FaviconUtils.createCircularIconGenerator(mContext);
+        mLargeIconBridge = new LargeIconBridge(profile);
+    }
+
+    public void destroy() {
+        mLargeIconBridge.destroy();
+    }
+}
diff --git a/chrome/browser/history_clusters/java/src/org/chromium/chrome/browser/history_clusters/HistoryClustersProvider.java b/chrome/browser/history_clusters/java/src/org/chromium/chrome/browser/history_clusters/HistoryClustersProvider.java
new file mode 100644
index 0000000..08a9e48e
--- /dev/null
+++ b/chrome/browser/history_clusters/java/src/org/chromium/chrome/browser/history_clusters/HistoryClustersProvider.java
@@ -0,0 +1,29 @@
+// 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.history_clusters;
+
+import androidx.annotation.NonNull;
+
+import org.chromium.base.Promise;
+
+/**
+ * Interface for a provider of history clusters data.
+ */
+interface HistoryClustersProvider {
+    /** Request a fixed number of clusters matching the given query. */
+    @NonNull
+    Promise<HistoryClustersResult> queryClusters(String query);
+
+    /**
+     * Request more clusters matching the most recent query. {@code query} must match that most
+     * recent query.
+     */
+    @NonNull
+    Promise<HistoryClustersResult> loadMoreClusters(String query);
+
+    /** Remove all the visits for the given cluster from the history database. */
+    @NonNull
+    Promise<Void> removeCluster(HistoryCluster clusterToRemove);
+}
\ No newline at end of file
diff --git a/chrome/browser/history_clusters/java/src/org/chromium/chrome/browser/history_clusters/HistoryClustersQueryManager.java b/chrome/browser/history_clusters/java/src/org/chromium/chrome/browser/history_clusters/HistoryClustersQueryManager.java
new file mode 100644
index 0000000..235d167e0
--- /dev/null
+++ b/chrome/browser/history_clusters/java/src/org/chromium/chrome/browser/history_clusters/HistoryClustersQueryManager.java
@@ -0,0 +1,64 @@
+// 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.history_clusters;
+
+import androidx.annotation.NonNull;
+
+import org.chromium.base.Promise;
+import org.chromium.base.supplier.OneShotCallback;
+import org.chromium.chrome.browser.profiles.Profile;
+
+/**
+ * Implementation of {@link HistoryClustersProvider} that encapsulates a HistoryClustersBridge,
+ * providing a safe async interface that insulates callers from the bridge's lifecycle.
+ */
+class HistoryClustersQueryManager implements HistoryClustersProvider {
+    private final HistoryClustersBridge mBridge;
+    private String mQuery;
+    private Promise<HistoryClustersResult> mPromise;
+    private OneShotCallback<Profile> mBridgeCreationCallback;
+
+    /**
+     * Construct a HistoryClustersQueryManager.
+     * @param profile The profile from which the coordinator should access history data.
+     */
+    HistoryClustersQueryManager(Profile profile) {
+        mBridge = new HistoryClustersBridge(profile);
+    }
+
+    /** Destroys the query manager and underlying bridge. */
+    void destroy() {
+        mBridge.destroy();
+    }
+
+    @Override
+    public Promise<HistoryClustersResult> queryClusters(String query) {
+        if (mPromise != null && !mPromise.isFulfilled()) {
+            mPromise.reject();
+        }
+        mPromise = new Promise<>();
+        mQuery = query;
+        if (mBridge != null) {
+            mBridge.queryClusters(mQuery, mPromise::fulfill);
+        }
+        return mPromise;
+    }
+
+    @Override
+    public Promise<HistoryClustersResult> loadMoreClusters(String query) {
+        assert mQuery != null;
+        assert mQuery.equals(query);
+        assert mPromise.isFulfilled();
+        mPromise = new Promise<>();
+        mBridge.loadMoreClusters(mQuery, mPromise::fulfill);
+        return mPromise;
+    }
+
+    @NonNull
+    @Override
+    public Promise<Void> removeCluster(HistoryCluster clusterToRemove) {
+        return new Promise<>();
+    }
+}
diff --git a/chrome/browser/media/cdm_document_service_impl.cc b/chrome/browser/media/cdm_document_service_impl.cc
index 16a5800a..7985b99d 100644
--- a/chrome/browser/media/cdm_document_service_impl.cc
+++ b/chrome/browser/media/cdm_document_service_impl.cc
@@ -51,6 +51,7 @@
 #include "base/win/security_util.h"
 #include "base/win/sid.h"
 #include "chrome/browser/media/cdm_pref_service_helper.h"
+#include "chrome/browser/media/media_foundation_service_monitor.h"
 #include "media/cdm/win/media_foundation_cdm.h"
 #include "sandbox/policy/win/lpac_capability.h"
 #endif  // BUILDFLAG(IS_WIN)
@@ -363,7 +364,36 @@
 
 void CdmDocumentServiceImpl::OnCdmEvent(media::CdmEvent event) {
   DVLOG(1) << __func__ << ": event=" << static_cast<int>(event);
-  NOTIMPLEMENTED();
+
+  // CdmDocumentServiceImpl is shared by all CDMs in the same RenderFrame.
+  //
+  // We choose to only report a significant playback at most once and an error
+  // at most once because:
+  // 1. A site could create many CDM instances, e.g. to prefetch licenses. This
+  //    could cause multiple errors to be reported.
+  // 2. The media::Renderer could be destroyed and then recreated as part of the
+  //    suspend/resume process (e.g. paused for long time).This could cause
+  //    multiple significant playback to be reported.
+  // In both cases, our data could be skewed if we don't throttle them.
+  //
+  // If an error happens after a significant playback both will be reported.
+  // This is fine since MediaFoundationServiceMonitor calculates a score.
+  switch (event) {
+    case media::CdmEvent::kSignificantPlayback:
+      if (!has_reported_significant_playback_) {
+        has_reported_significant_playback_ = true;
+        MediaFoundationServiceMonitor::GetInstance()->OnSignificantPlayback();
+      }
+      break;
+    case media::CdmEvent::kPlaybackError:
+      [[fallthrough]];
+    case media::CdmEvent::kCdmError:
+      if (!has_reported_cdm_error_) {
+        has_reported_cdm_error_ = true;
+        MediaFoundationServiceMonitor::GetInstance()->OnPlaybackOrCdmError();
+      }
+      break;
+  }
 }
 
 // This function goes over each folder located under the MediaFoundationCdm
diff --git a/chrome/browser/media/cdm_document_service_impl.h b/chrome/browser/media/cdm_document_service_impl.h
index 951f9853..e18a196 100644
--- a/chrome/browser/media/cdm_document_service_impl.h
+++ b/chrome/browser/media/cdm_document_service_impl.h
@@ -28,6 +28,11 @@
 // Implements media::mojom::CdmDocumentService. Can only be used on the
 // UI thread because PlatformVerificationFlow and the pref service lives on the
 // UI thread.
+// Ownership Note: There's one CdmDocumentServiceImpl per RenderFrame per
+// service type ( MediaFoundationService or CdmService). For
+// MediaFoundationService's case, this can be seen in the ownership chain of
+// InterfaceFactoryImpl -> MediaFoundationCdmFactory -> MojoCdmHelper
+// -> mojo::Remote<mojom::CdmDocumentService>.
 class CdmDocumentServiceImpl final
     : public content::DocumentService<media::mojom::CdmDocumentService> {
  public:
@@ -89,6 +94,12 @@
       platform_verification_flow_;
 #endif
 
+#if BUILDFLAG(IS_WIN)
+  // See comments in OnCdmEvent() implementation.
+  bool has_reported_cdm_error_ = false;
+  bool has_reported_significant_playback_ = false;
+#endif
+
   const raw_ptr<content::RenderFrameHost> render_frame_host_;
   base::WeakPtrFactory<CdmDocumentServiceImpl> weak_factory_{this};
 };
diff --git a/chrome/browser/media/media_foundation_service_monitor.cc b/chrome/browser/media/media_foundation_service_monitor.cc
index ffb5599..7ec8905 100644
--- a/chrome/browser/media/media_foundation_service_monitor.cc
+++ b/chrome/browser/media/media_foundation_service_monitor.cc
@@ -5,11 +5,23 @@
 #include "chrome/browser/media/media_foundation_service_monitor.h"
 
 #include "base/logging.h"
+#include "base/power_monitor/power_monitor.h"
 #include "content/public/browser/browser_thread.h"
 #include "content/public/browser/cdm_registry.h"
 #include "media/mojo/mojom/media_foundation_service.mojom.h"
+#include "ui/display/screen.h"
 
-const int kMaxNumberFailures = 2;
+namespace {
+
+constexpr int kMaxNumberOfSamples = 20;
+constexpr auto kGracePeriod = base::Seconds(2);
+constexpr double kMaxAverageFailureScore = 0.1;  // Two failures out of 20.
+
+constexpr int kSignificantPlayback = 0;  // This must always be zero.
+constexpr int kPlaybackOrCdmError = 1;
+constexpr int kCrash = 1;
+
+}  // namespace
 
 // static
 MediaFoundationServiceMonitor* MediaFoundationServiceMonitor::GetInstance() {
@@ -17,27 +29,89 @@
   return monitor;
 }
 
-MediaFoundationServiceMonitor::MediaFoundationServiceMonitor() {
+MediaFoundationServiceMonitor::MediaFoundationServiceMonitor()
+    : samples_(kMaxNumberOfSamples) {
   DVLOG(1) << __func__;
   DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
+
+  // Initialize samples with success cases so the average score won't be
+  // dominated by one error.
+  for (int i = 0; i < kMaxNumberOfSamples; ++i)
+    AddSample(kSignificantPlayback);
+
   content::ServiceProcessHost::AddObserver(this);
+  base::PowerMonitor::AddPowerSuspendObserver(this);
+  display::Screen::GetScreen()->AddObserver(this);
 }
 
 MediaFoundationServiceMonitor::~MediaFoundationServiceMonitor() = default;
 
 void MediaFoundationServiceMonitor::OnServiceProcessCrashed(
     const content::ServiceProcessInfo& info) {
+  DVLOG(1) << __func__;
   DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
 
   // Only interested in MediaFoundationService process.
   if (!info.IsService<media::mojom::MediaFoundationServiceBroker>())
     return;
 
-  DLOG(ERROR) << __func__ << ": MediaFoundationService process crashed!";
+  // Not checking `last_power_or_display_change_time_`; crashes are always bad.
+  DVLOG(1) << __func__ << ": MediaFoundationService process crashed!";
+  AddSample(kCrash);
+}
 
-  num_crashes_++;
+void MediaFoundationServiceMonitor::OnSuspend() {
+  OnPowerOrDisplayChange();
+}
 
-  if (num_crashes_ >= kMaxNumberFailures) {
+void MediaFoundationServiceMonitor::OnResume() {
+  OnPowerOrDisplayChange();
+}
+
+void MediaFoundationServiceMonitor::OnDisplayAdded(
+    const display::Display& /*new_display*/) {
+  OnPowerOrDisplayChange();
+}
+void MediaFoundationServiceMonitor::OnDidRemoveDisplays() {
+  OnPowerOrDisplayChange();
+}
+void MediaFoundationServiceMonitor::OnDisplayMetricsChanged(
+    const display::Display& /*display*/,
+    uint32_t /*changed_metrics*/) {
+  OnPowerOrDisplayChange();
+}
+
+void MediaFoundationServiceMonitor::OnSignificantPlayback() {
+  DVLOG(1) << __func__;
+  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
+
+  AddSample(kSignificantPlayback);
+}
+
+void MediaFoundationServiceMonitor::OnPlaybackOrCdmError() {
+  DVLOG(1) << __func__;
+  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
+
+  if (base::Time::Now() - last_power_or_display_change_time_ < kGracePeriod) {
+    DVLOG(1) << "Playback or CDM error ignored since it happened right after "
+                "a power or display change.";
+    return;
+  }
+
+  AddSample(kPlaybackOrCdmError);
+}
+
+void MediaFoundationServiceMonitor::OnPowerOrDisplayChange() {
+  DVLOG(1) << __func__;
+  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
+
+  last_power_or_display_change_time_ = base::Time::Now();
+}
+
+void MediaFoundationServiceMonitor::AddSample(int failure_score) {
+  samples_.AddSample(failure_score);
+
+  if (samples_.GetUnroundedAverage() >= kMaxAverageFailureScore) {
     content::CdmRegistry::GetInstance()->DisableHardwareSecureCdms();
   }
 }
diff --git a/chrome/browser/media/media_foundation_service_monitor.h b/chrome/browser/media/media_foundation_service_monitor.h
index 0a69232..b7ef3d94 100644
--- a/chrome/browser/media/media_foundation_service_monitor.h
+++ b/chrome/browser/media/media_foundation_service_monitor.h
@@ -5,10 +5,16 @@
 #ifndef CHROME_BROWSER_MEDIA_MEDIA_FOUNDATION_SERVICE_MONITOR_H_
 #define CHROME_BROWSER_MEDIA_MEDIA_FOUNDATION_SERVICE_MONITOR_H_
 
+#include "base/power_monitor/moving_average.h"
+#include "base/power_monitor/power_observer.h"
+#include "base/time/time.h"
 #include "content/public/browser/service_process_host.h"
+#include "ui/display/display_observer.h"
 
-class MediaFoundationServiceMonitor
-    : public content::ServiceProcessHost::Observer {
+class MediaFoundationServiceMonitor final
+    : public content::ServiceProcessHost::Observer,
+      public base::PowerSuspendObserver,
+      public display::DisplayObserver {
  public:
   // Returns the MediaFoundationServiceMonitor singleton.
   static MediaFoundationServiceMonitor* GetInstance();
@@ -18,15 +24,43 @@
       const MediaFoundationServiceMonitor&) = delete;
 
   // ServiceProcessHost::Observer implementation.
-  void OnServiceProcessCrashed(
-      const content::ServiceProcessInfo& info) override;
+  void OnServiceProcessCrashed(const content::ServiceProcessInfo& info) final;
+
+  // base::PowerSuspendObserver implementation.
+  void OnSuspend() final;
+  void OnResume() final;
+
+  // display::DisplayObserver implementation.
+  void OnDisplayAdded(const display::Display& new_display) final;
+  void OnDidRemoveDisplays() final;
+  void OnDisplayMetricsChanged(const display::Display& display,
+                               uint32_t changed_metrics) final;
+
+  // Called when a significant playback or error happened when using
+  // MediaFoundationCdm.
+  void OnSignificantPlayback();
+  void OnPlaybackOrCdmError();
 
  private:
   // Make constructor/destructor private since this is a singleton.
   MediaFoundationServiceMonitor();
-  ~MediaFoundationServiceMonitor() override;
+  ~MediaFoundationServiceMonitor() final;
 
-  int num_crashes_ = 0;
+  void OnPowerOrDisplayChange();
+
+  // Adds a sample with failure score. Zero means success. The higher the value
+  // the more severe the error is. See the .cc file for details.
+  void AddSample(int failure_score);
+
+  // Last time when a power or display event happened. This is used to ignore
+  // playback or CDM errors caused by those events. For example, playback
+  // failure caused by user plugging in a non-HDCP monitor, but the content
+  // requires HDCP enforcement.
+  base::Time last_power_or_display_change_time_;
+
+  // Keep track the last fixed length reported samples (scores). The average
+  // score is used to decide whether to disable hardware secure decryption.
+  base::MovingAverage samples_;
 };
 
 #endif  // CHROME_BROWSER_MEDIA_MEDIA_FOUNDATION_SERVICE_MONITOR_H_
diff --git a/chrome/browser/media/router/media_router_feature.cc b/chrome/browser/media/router/media_router_feature.cc
index 8400367..2695c7b 100644
--- a/chrome/browser/media/router/media_router_feature.cc
+++ b/chrome/browser/media/router/media_router_feature.cc
@@ -101,7 +101,7 @@
 }
 
 void RegisterProfilePrefs(PrefRegistrySimple* registry) {
-  // TODO(imcheng): Migrate existing Media Router prefs to here.
+  // TODO(crbug.com/1308056): Migrate existing Media Router prefs to here.
   registry->RegisterStringPref(prefs::kMediaRouterReceiverIdHashToken, "",
                                PrefRegistry::PUBLIC);
 }
diff --git a/chrome/browser/navigation_predictor/anchor_element_preloader.cc b/chrome/browser/navigation_predictor/anchor_element_preloader.cc
new file mode 100644
index 0000000..550ca3f
--- /dev/null
+++ b/chrome/browser/navigation_predictor/anchor_element_preloader.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 "chrome/browser/navigation_predictor/anchor_element_preloader.h"
+#include "chrome/browser/predictors/loading_predictor.h"
+#include "chrome/browser/predictors/loading_predictor_factory.h"
+#include "content/public/browser/browser_context.h"
+
+AnchorElementPreloader::AnchorElementPreloader(
+    content::RenderFrameHost* render_frame_host,
+    mojo::PendingReceiver<blink::mojom::AnchorElementInteractionHost> receiver)
+    : content::DocumentService<blink::mojom::AnchorElementInteractionHost>(
+          render_frame_host,
+          std::move(receiver)) {}
+
+void AnchorElementPreloader::Create(
+    content::RenderFrameHost* render_frame_host,
+    mojo::PendingReceiver<blink::mojom::AnchorElementInteractionHost>
+        receiver) {
+  // The object is bound to the lifetime of the |render_frame_host| and the mojo
+  // connection. See DocumentService for details.
+  new AnchorElementPreloader(render_frame_host, std::move(receiver));
+}
+
+void AnchorElementPreloader::OnPointerDown(const GURL& target) {
+  auto* loading_predictor = predictors::LoadingPredictorFactory::GetForProfile(
+      Profile::FromBrowserContext(render_frame_host()->GetBrowserContext()));
+
+  if (!loading_predictor) {
+    return;
+  }
+
+  net::SchemefulSite schemeful_site(target);
+  net::NetworkIsolationKey network_isolation_key(schemeful_site,
+                                                 schemeful_site);
+  loading_predictor->PreconnectURLIfAllowed(target, /*allow_credentials=*/true,
+                                            network_isolation_key);
+}
diff --git a/chrome/browser/navigation_predictor/anchor_element_preloader.h b/chrome/browser/navigation_predictor/anchor_element_preloader.h
new file mode 100644
index 0000000..a6962c59
--- /dev/null
+++ b/chrome/browser/navigation_predictor/anchor_element_preloader.h
@@ -0,0 +1,28 @@
+// 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_NAVIGATION_PREDICTOR_ANCHOR_ELEMENT_PRELOADER_H_
+#define CHROME_BROWSER_NAVIGATION_PREDICTOR_ANCHOR_ELEMENT_PRELOADER_H_
+
+#include "content/public/browser/document_service.h"
+#include "third_party/blink/public/mojom/loader/anchor_element_interaction_host.mojom.h"
+
+class AnchorElementPreloader
+    : content::DocumentService<blink::mojom::AnchorElementInteractionHost> {
+ public:
+  static void Create(
+      content::RenderFrameHost* render_frame_host,
+      mojo::PendingReceiver<blink::mojom::AnchorElementInteractionHost>
+          receiver);
+
+ private:
+  AnchorElementPreloader(
+      content::RenderFrameHost* render_frame_host,
+      mojo::PendingReceiver<blink::mojom::AnchorElementInteractionHost>
+          receiver);
+  // Preconnects to the given URL `target`.
+  void OnPointerDown(const GURL& target) override;
+};
+
+#endif  // CHROME_BROWSER_NAVIGATION_PREDICTOR_ANCHOR_ELEMENT_PRELOADER_H_
diff --git a/chrome/browser/offline_pages/android/evaluation/offline_page_evaluation_bridge.cc b/chrome/browser/offline_pages/android/evaluation/offline_page_evaluation_bridge.cc
index 1513e85b..8da2587 100644
--- a/chrome/browser/offline_pages/android/evaluation/offline_page_evaluation_bridge.cc
+++ b/chrome/browser/offline_pages/android/evaluation/offline_page_evaluation_bridge.cc
@@ -14,8 +14,8 @@
 #include "base/android/jni_array.h"
 #include "base/android/jni_string.h"
 #include "base/bind.h"
-#include "base/task/post_task.h"
 #include "base/task/sequenced_task_runner.h"
+#include "base/task/thread_pool.h"
 #include "chrome/android/chrome_jni_headers/OfflinePageEvaluationBridge_jni.h"
 #include "chrome/browser/browser_process.h"
 #include "chrome/browser/offline_pages/android/background_scheduler_bridge.h"
@@ -140,7 +140,7 @@
     std::unique_ptr<OfflinerPolicy> policy,
     std::unique_ptr<Offliner> offliner) {
   scoped_refptr<base::SequencedTaskRunner> background_task_runner =
-      base::CreateSequencedTaskRunner({base::MayBlock()});
+      base::ThreadPool::CreateSequencedTaskRunner({base::MayBlock()});
   Profile* profile = Profile::FromBrowserContext(context);
   base::FilePath queue_store_path =
       profile->GetPath().Append(kTestRequestQueueDirname);
diff --git a/chrome/browser/prefs/browser_prefs.cc b/chrome/browser/prefs/browser_prefs.cc
index 4bf67777..f5eb1ef8d 100644
--- a/chrome/browser/prefs/browser_prefs.cc
+++ b/chrome/browser/prefs/browser_prefs.cc
@@ -261,6 +261,7 @@
 #include "chrome/browser/ui/webui/whats_new/whats_new_ui.h"
 #include "chrome/browser/upgrade_detector/upgrade_detector.h"
 #include "components/live_caption/live_caption_controller.h"
+#include "components/media_router/common/pref_names.h"
 #include "components/ntp_tiles/custom_links_manager_impl.h"
 #endif  // BUILDFLAG(IS_ANDROID)
 
@@ -1502,6 +1503,23 @@
 
   registry->RegisterBooleanPref(prefs::kPrivacyGuideViewed, false);
 
+// TODO(crbug.com/1308056): Migrate media_router prefs to
+// media_router::RegisterProfilePrefs().
+#if !BUILDFLAG(IS_ANDROID)
+  registry->RegisterBooleanPref(
+      media_router::prefs::kMediaRouterMediaRemotingEnabled, true);
+  registry->RegisterListPref(
+      media_router::prefs::kMediaRouterTabMirroringSources);
+#endif
+
+// TODO(crbug.com/1308053): Register it on ChromeOS after Cast+GMC ships on
+// ChromeOS.
+#if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_CHROMEOS)
+  registry->RegisterBooleanPref(
+      media_router::prefs::kMediaRouterShowCastSessionsStartedByOtherDevices,
+      true);
+#endif
+
   RegisterProfilePrefsForMigration(registry);
 }
 
diff --git a/chrome/browser/printing/print_backend_service_manager.h b/chrome/browser/printing/print_backend_service_manager.h
index d5a1ca8..5897e1d8 100644
--- a/chrome/browser/printing/print_backend_service_manager.h
+++ b/chrome/browser/printing/print_backend_service_manager.h
@@ -150,6 +150,7 @@
 
  private:
   friend base::NoDestructor<PrintBackendServiceManager>;
+  friend class PrintBackendPrintBrowserTestBase;
   FRIEND_TEST_ALL_PREFIXES(PrintBackendServiceManagerTest,
                            IsIdleTimeoutUpdateNeededForRegisteredClient);
   FRIEND_TEST_ALL_PREFIXES(PrintBackendServiceManagerTest,
diff --git a/chrome/browser/printing/print_browsertest.cc b/chrome/browser/printing/print_browsertest.cc
index 69ef85cf..71b3f0e 100644
--- a/chrome/browser/printing/print_browsertest.cc
+++ b/chrome/browser/printing/print_browsertest.cc
@@ -64,6 +64,7 @@
 #include "printing/backend/test_print_backend.h"
 #include "printing/buildflags/buildflags.h"
 #include "printing/mojom/print.mojom.h"
+#include "printing/page_setup.h"
 #include "printing/print_settings.h"
 #include "printing/printing_context.h"
 #include "printing/printing_context_factory_for_test.h"
@@ -75,6 +76,7 @@
 #include "third_party/blink/public/common/associated_interfaces/associated_interface_provider.h"
 #include "third_party/blink/public/common/features.h"
 #include "third_party/blink/public/common/scheduler/web_scheduler_tracked_feature.h"
+#include "ui/gfx/geometry/rect.h"
 #include "ui/gfx/geometry/size.h"
 
 #if BUILDFLAG(ENABLE_OOP_PRINTING)
@@ -91,6 +93,12 @@
 #if BUILDFLAG(ENABLE_OOP_PRINTING)
 using ErrorCheckCallback =
     base::RepeatingCallback<void(mojom::ResultCode result)>;
+using OnDidUseDefaultSettingsCallback =
+    base::RepeatingCallback<void(mojom::ResultCode result)>;
+#if BUILDFLAG(IS_WIN)
+using OnDidAskUserForSettingsCallback =
+    base::RepeatingCallback<void(mojom::ResultCode result)>;
+#endif
 using OnDidStartPrintingCallback =
     base::RepeatingCallback<void(mojom::ResultCode result,
                                  PrintJob* print_job)>;
@@ -117,6 +125,10 @@
 //       processing was done before possibly quitting the test run loop.
 struct TestPrintCallbacks {
   ErrorCheckCallback error_check_callback;
+  OnDidUseDefaultSettingsCallback did_use_default_settings_callback;
+#if BUILDFLAG(IS_WIN)
+  OnDidAskUserForSettingsCallback did_ask_user_for_settings_callback;
+#endif
   OnDidStartPrintingCallback did_start_printing_callback;
 #if BUILDFLAG(IS_WIN)
   OnDidRenderPrintedPageCallback did_render_printed_page_callback;
@@ -522,6 +534,10 @@
 
   PrintSettings* snooped_settings() { return snooped_settings_.get(); }
 
+  const absl::optional<bool>& print_now_result() const {
+    return print_now_result_;
+  }
+
   static TestPrintViewManager* CreateForWebContents(
       content::WebContents* web_contents) {
     auto manager = std::make_unique<TestPrintViewManager>(web_contents);
@@ -531,6 +547,13 @@
     return manager_ptr;
   }
 
+  // `PrintViewManagerBase` overrides.
+  bool PrintNow(content::RenderFrameHost* rfh) override {
+    print_now_result_ = PrintViewManager::PrintNow(rfh);
+    return *print_now_result_;
+  }
+  void ShowInvalidPrinterSettingsError() override {}
+
  protected:
   base::RunLoop* run_loop_ = nullptr;
 
@@ -553,6 +576,7 @@
   }
 
   std::unique_ptr<PrintSettings> snooped_settings_;
+  absl::optional<bool> print_now_result_;
 };
 
 class TestPrintViewManagerForDLP : public TestPrintViewManager {
@@ -2107,6 +2131,34 @@
   ~TestPrintJobWorker() override = default;
 
  private:
+  void OnDidUseDefaultSettings(
+      SettingsCallback callback,
+      mojom::PrintSettingsResultPtr print_settings) override {
+    DVLOG(1) << "Observed: use default settings";
+    mojom::ResultCode result = print_settings->is_result_code()
+                                   ? print_settings->get_result_code()
+                                   : mojom::ResultCode::kSuccess;
+    callbacks_->error_check_callback.Run(result);
+    PrintJobWorkerOop::OnDidUseDefaultSettings(std::move(callback),
+                                               std::move(print_settings));
+    callbacks_->did_use_default_settings_callback.Run(result);
+  }
+
+#if BUILDFLAG(IS_WIN)
+  void OnDidAskUserForSettings(
+      SettingsCallback callback,
+      mojom::PrintSettingsResultPtr print_settings) override {
+    DVLOG(1) << "Observed: ask user for settings";
+    mojom::ResultCode result = print_settings->is_result_code()
+                                   ? print_settings->get_result_code()
+                                   : mojom::ResultCode::kSuccess;
+    callbacks_->error_check_callback.Run(result);
+    PrintJobWorkerOop::OnDidAskUserForSettings(std::move(callback),
+                                               std::move(print_settings));
+    callbacks_->did_ask_user_for_settings_callback.Run(result);
+  }
+#endif  // BUILDFLAG(IS_WIN)
+
   void OnDidStartPrinting(mojom::ResultCode result) override {
     DVLOG(1) << "Observed: start printing of document";
     callbacks_->error_check_callback.Run(result);
@@ -2167,6 +2219,16 @@
       test_print_callbacks_.error_check_callback =
           base::BindRepeating(&PrintBackendPrintBrowserTestBase::ErrorCheck,
                               base::Unretained(this));
+      test_print_callbacks_.did_use_default_settings_callback =
+          base::BindRepeating(
+              &PrintBackendPrintBrowserTestBase::OnDidUseDefaultSettings,
+              base::Unretained(this));
+#if BUILDFLAG(IS_WIN)
+      test_print_callbacks_.did_ask_user_for_settings_callback =
+          base::BindRepeating(
+              &PrintBackendPrintBrowserTestBase::OnDidAskUserForSettings,
+              base::Unretained(this));
+#endif
       test_print_callbacks_.did_start_printing_callback = base::BindRepeating(
           &PrintBackendPrintBrowserTestBase::OnDidStartPrinting,
           base::Unretained(this));
@@ -2215,6 +2277,12 @@
     PrintBackend::SetPrintBackendForTesting(/*print_backend=*/nullptr);
 #if BUILDFLAG(ENABLE_OOP_PRINTING)
     PrinterQuery::SetCreatePrintJobWorkerCallbackForTest(/*callback=*/nullptr);
+    if (UseService()) {
+      // Check that there is never a straggler client registration.
+      EXPECT_EQ(
+          PrintBackendServiceManager::GetInstance().GetClientsRegisteredCount(),
+          0u);
+    }
     PrintBackendServiceManager::ResetForTesting();
 #endif
   }
@@ -2276,6 +2344,16 @@
 
   void PrimeAsRepeatingErrorGenerator() { reset_errors_after_check_ = false; }
 
+  void PrimeForFailInUseDefaultSettings() {
+    test_printing_context_factory_.SetFailErrorOnUseDefaultSettings();
+  }
+
+#if BUILDFLAG(IS_WIN)
+  void PrimeForCancelInAskUserForSettings() {
+    test_printing_context_factory_.SetCancelErrorOnAskUserForSettings();
+  }
+#endif
+
   void PrimeForAccessDeniedErrorsInNewDocument() {
     test_printing_context_factory_.SetAccessDeniedErrorOnNewDocument(
         /*cause_errors=*/true);
@@ -2293,6 +2371,16 @@
         /*cause_errors=*/true);
   }
 
+  mojom::ResultCode use_default_settings_result() const {
+    return use_default_settings_result_;
+  }
+
+#if BUILDFLAG(IS_WIN)
+  mojom::ResultCode ask_user_for_settings_result() const {
+    return ask_user_for_settings_result_;
+  }
+#endif
+
   mojom::ResultCode start_printing_result() const {
     return start_printing_result_;
   }
@@ -2322,9 +2410,19 @@
       auto context =
           std::make_unique<TestPrintingContext>(delegate, skip_system_calls);
 
+      // Setup a sample page setup, which is needed to pass checks in
+      // `PrintRenderFrameHelper` that the print params are valid.
+      constexpr gfx::Size kPhysicalSize = gfx::Size(200, 200);
+      constexpr gfx::Rect kPrintableArea = gfx::Rect(0, 0, 200, 200);
+      const PageMargins kRequestedMargins(0, 0, 5, 5, 5, 5);
+      const PageSetup kPageSetup(kPhysicalSize, kPrintableArea,
+                                 kRequestedMargins, /*forced_margins=*/false,
+                                 /*text_height=*/0);
+
       auto settings = std::make_unique<PrintSettings>();
       settings->set_copies(kTestPrintSettingsCopies);
       settings->set_dpi(kTestPrintingDpi);
+      settings->set_page_setup_device_units(kPageSetup);
       settings->set_device_name(
           base::ASCIIToUTF16(base::StringPiece(printer_name_)));
       context->SetDeviceSettings(printer_name_, std::move(settings));
@@ -2338,6 +2436,13 @@
       if (access_denied_errors_for_document_done_)
         context->SetDocumentDoneBlockedByPermissions();
 
+      if (fail_on_use_default_settings_)
+        context->SetUseDefaultSettingsFails();
+#if BUILDFLAG(IS_WIN)
+      if (cancel_on_ask_user_for_settings_)
+        context->SetAskUserForSettingsCanceled();
+#endif
+
       return std::move(context);
     }
 
@@ -2359,6 +2464,16 @@
       access_denied_errors_for_document_done_ = cause_errors;
     }
 
+    void SetFailErrorOnUseDefaultSettings() {
+      fail_on_use_default_settings_ = true;
+    }
+
+#if BUILDFLAG(IS_WIN)
+    void SetCancelErrorOnAskUserForSettings() {
+      cancel_on_ask_user_for_settings_ = true;
+    }
+#endif
+
    private:
     std::string printer_name_;
     bool access_denied_errors_for_new_document_ = false;
@@ -2366,6 +2481,10 @@
     bool access_denied_errors_for_render_page_ = false;
 #endif
     bool access_denied_errors_for_document_done_ = false;
+    bool fail_on_use_default_settings_ = false;
+#if BUILDFLAG(IS_WIN)
+    bool cancel_on_ask_user_for_settings_ = false;
+#endif
   };
 
 #if BUILDFLAG(ENABLE_OOP_PRINTING)
@@ -2382,6 +2501,18 @@
       ResetForNoAccessDeniedErrors();
   }
 
+  void OnDidUseDefaultSettings(mojom::ResultCode result) {
+    use_default_settings_result_ = result;
+    CheckForQuit();
+  }
+
+#if BUILDFLAG(IS_WIN)
+  void OnDidAskUserForSettings(mojom::ResultCode result) {
+    ask_user_for_settings_result_ = result;
+    CheckForQuit();
+  }
+#endif
+
   void OnDidStartPrinting(mojom::ResultCode result, PrintJob* print_job) {
     start_printing_result_ = result;
     print_job_ = print_job;
@@ -2439,6 +2570,10 @@
 #endif  // BUILDFLAG(ENABLE_OOP_PRINTING)
   PrintJob* print_job_ = nullptr;
   bool reset_errors_after_check_ = true;
+  mojom::ResultCode use_default_settings_result_ = mojom::ResultCode::kFailed;
+#if BUILDFLAG(IS_WIN)
+  mojom::ResultCode ask_user_for_settings_result_ = mojom::ResultCode::kFailed;
+#endif
   mojom::ResultCode start_printing_result_ = mojom::ResultCode::kFailed;
 #if BUILDFLAG(IS_WIN)
   mojom::ResultCode render_printed_page_result_ = mojom::ResultCode::kFailed;
@@ -2691,8 +2826,127 @@
   EXPECT_TRUE(error_dialog_shown());
   EXPECT_TRUE(stop_invoked());
 }
+
+IN_PROC_BROWSER_TEST_F(PrintBackendPrintBrowserTestService, StartBasicPrint) {
+  AddPrinter("printer1");
+  SetPrinterNameForSubsequentContexts("printer1");
+
+  ASSERT_TRUE(embedded_test_server()->Started());
+  GURL url(embedded_test_server()->GetURL("/printing/test3.html"));
+  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));
+
+  content::WebContents* web_contents =
+      browser()->tab_strip_model()->GetActiveWebContents();
+  ASSERT_TRUE(web_contents);
+  SetUpPrintViewManager(web_contents);
+  StartBasicPrint(web_contents);
+
+  // The test will get the default settings followed by asking the user for
+  // settings.  After that a print job will be started, with a page getting
+  // rendered, and finally the document done notification.  Wait for a call to
+  // `Stop()` to ensure print job wrap-up finished cleanly before completing
+  // the test.  This results in a total of 6 calls.
+  SetNumExpectedMessages(/*num=*/6);
+
+  WaitUntilCallbackReceived();
+
+  EXPECT_EQ(use_default_settings_result(), mojom::ResultCode::kSuccess);
+  EXPECT_EQ(ask_user_for_settings_result(), mojom::ResultCode::kSuccess);
+  EXPECT_EQ(start_printing_result(), mojom::ResultCode::kSuccess);
+  EXPECT_EQ(render_printed_page_result(), mojom::ResultCode::kSuccess);
+  EXPECT_EQ(render_printed_page_count(), 1);
+  EXPECT_EQ(document_done_result(), mojom::ResultCode::kSuccess);
+  EXPECT_TRUE(stop_invoked());
+}
+
+IN_PROC_BROWSER_TEST_F(PrintBackendPrintBrowserTestService,
+                       StartBasicPrintCancel) {
+  PrimeForCancelInAskUserForSettings();
+
+  ASSERT_TRUE(embedded_test_server()->Started());
+  GURL url(embedded_test_server()->GetURL("/printing/test3.html"));
+  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));
+
+  content::WebContents* web_contents =
+      browser()->tab_strip_model()->GetActiveWebContents();
+  ASSERT_TRUE(web_contents);
+  SetUpPrintViewManager(web_contents);
+  StartBasicPrint(web_contents);
+
+  // The test will get the default settings followed by asking the user for
+  // settings.  Since this pretends the user canceled from that, no further
+  // printing calls will be made.  Wait for a call to `Stop()` to ensure print
+  // job wrap-up finished cleanly before completing
+  // the test. This results in a total of 3 calls.
+  SetNumExpectedMessages(/*num=*/3);
+
+  WaitUntilCallbackReceived();
+
+  EXPECT_EQ(use_default_settings_result(), mojom::ResultCode::kSuccess);
+  EXPECT_EQ(ask_user_for_settings_result(), mojom::ResultCode::kCanceled);
+  EXPECT_TRUE(stop_invoked());
+}
+
+IN_PROC_BROWSER_TEST_F(PrintBackendPrintBrowserTestService,
+                       StartBasicPrintConcurrent) {
+  ASSERT_TRUE(embedded_test_server()->Started());
+  GURL url(embedded_test_server()->GetURL("/printing/test3.html"));
+  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));
+
+  content::WebContents* web_contents =
+      browser()->tab_strip_model()->GetActiveWebContents();
+  ASSERT_TRUE(web_contents);
+  TestPrintViewManager* print_view_manager =
+      TestPrintViewManager::CreateForWebContents(web_contents);
+
+  // Pretend that a window has started a system print.
+  absl::optional<uint32_t> client_id =
+      PrintBackendServiceManager::GetInstance().RegisterQueryWithUiClient();
+  ASSERT_TRUE(client_id.has_value());
+
+  // Now initiate a system print that would exist concurrently with that.
+  StartBasicPrint(web_contents);
+
+  // On Windows, concurrent system print is not allowed.
+  // TODO(crbug.com/809738):  Once Linux Wayland can be made to have a system
+  // dialog be modal against an application window in the browser process,
+  // update this to illustrate that concurrent system prints area allowed for
+  // Linux.
+  const absl::optional<bool>& result = print_view_manager->print_now_result();
+  ASSERT_TRUE(result.has_value());
+  EXPECT_FALSE(*result);
+
+  // Cleanup before test shutdown.
+  PrintBackendServiceManager::GetInstance().UnregisterClient(*client_id);
+}
 #endif  // BUILDFLAG(IS_WIN)
 
+IN_PROC_BROWSER_TEST_F(PrintBackendPrintBrowserTestService,
+                       StartBasicPrintUseDefaultFails) {
+  PrimeForFailInUseDefaultSettings();
+
+  ASSERT_TRUE(embedded_test_server()->Started());
+  GURL url(embedded_test_server()->GetURL("/printing/test3.html"));
+  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));
+
+  content::WebContents* web_contents =
+      browser()->tab_strip_model()->GetActiveWebContents();
+  ASSERT_TRUE(web_contents);
+  SetUpPrintViewManager(web_contents);
+  StartBasicPrint(web_contents);
+
+  // The test will fail getting the default settings, aborting the rest of
+  // printing.  Wait for a call to `Stop()` to ensure print job wrap-up
+  // finished cleanly before completing the test. This results in a total of
+  // 2 calls.
+  SetNumExpectedMessages(/*num=*/2);
+
+  WaitUntilCallbackReceived();
+
+  EXPECT_EQ(use_default_settings_result(), mojom::ResultCode::kFailed);
+  EXPECT_TRUE(stop_invoked());
+}
+
 #endif  //  BUILDFLAG(ENABLE_OOP_PRINTING)
 
 #endif  // !BUILDFLAG(IS_CHROMEOS)
diff --git a/chrome/browser/printing/print_job_worker.cc b/chrome/browser/printing/print_job_worker.cc
index 2730599..b11b8f3 100644
--- a/chrome/browser/printing/print_job_worker.cc
+++ b/chrome/browser/printing/print_job_worker.cc
@@ -166,15 +166,10 @@
   // When we delegate to a destination, we don't ask the user for settings.
   // TODO(mad): Ask the destination for settings.
   if (ask_user_for_settings) {
-    content::GetUIThreadTaskRunner({})->PostTask(
-        FROM_HERE,
-        base::BindOnce(&PrintJobWorker::GetSettingsWithUI,
-                       base::Unretained(this), document_page_count,
-                       has_selection, is_scripted, std::move(callback)));
+    InvokeGetSettingsWithUI(document_page_count, has_selection, is_scripted,
+                            std::move(callback));
   } else {
-    content::GetUIThreadTaskRunner({})->PostTask(
-        FROM_HERE, base::BindOnce(&PrintJobWorker::UseDefaultSettings,
-                                  base::Unretained(this), std::move(callback)));
+    InvokeUseDefaultSettings(std::move(callback));
   }
 }
 
@@ -262,6 +257,23 @@
   std::move(callback).Run(printing_context_->TakeAndResetSettings(), result);
 }
 
+void PrintJobWorker::InvokeUseDefaultSettings(SettingsCallback callback) {
+  content::GetUIThreadTaskRunner({})->PostTask(
+      FROM_HERE, base::BindOnce(&PrintJobWorker::UseDefaultSettings,
+                                base::Unretained(this), std::move(callback)));
+}
+
+void PrintJobWorker::InvokeGetSettingsWithUI(uint32_t document_page_count,
+                                             bool has_selection,
+                                             bool is_scripted,
+                                             SettingsCallback callback) {
+  content::GetUIThreadTaskRunner({})->PostTask(
+      FROM_HERE,
+      base::BindOnce(&PrintJobWorker::GetSettingsWithUI, base::Unretained(this),
+                     document_page_count, has_selection, is_scripted,
+                     std::move(callback)));
+}
+
 void PrintJobWorker::GetSettingsWithUI(uint32_t document_page_count,
                                        bool has_selection,
                                        bool is_scripted,
diff --git a/chrome/browser/printing/print_job_worker.h b/chrome/browser/printing/print_job_worker.h
index 465afca..975b7b6 100644
--- a/chrome/browser/printing/print_job_worker.h
+++ b/chrome/browser/printing/print_job_worker.h
@@ -134,6 +134,14 @@
   // Reports settings back to `callback`.
   void GetSettingsDone(SettingsCallback callback, mojom::ResultCode result);
 
+  // Helper functions to invoke the desired way of getting system print
+  // settings.
+  virtual void InvokeUseDefaultSettings(SettingsCallback callback);
+  virtual void InvokeGetSettingsWithUI(uint32_t document_page_count,
+                                       bool has_selection,
+                                       bool is_scripted,
+                                       SettingsCallback callback);
+
   // Called on the UI thread to update the print settings.
   virtual void UpdatePrintSettings(base::Value new_settings,
                                    SettingsCallback callback);
diff --git a/chrome/browser/printing/print_job_worker_oop.cc b/chrome/browser/printing/print_job_worker_oop.cc
index 52a13c0c..56232bf 100644
--- a/chrome/browser/printing/print_job_worker_oop.cc
+++ b/chrome/browser/printing/print_job_worker_oop.cc
@@ -22,6 +22,7 @@
 #include "third_party/abseil-cpp/absl/types/optional.h"
 
 #if BUILDFLAG(IS_WIN)
+#include "content/public/browser/web_contents.h"
 #include "printing/printed_page_win.h"
 #endif
 
@@ -75,6 +76,52 @@
                                 document_name));
 }
 
+void PrintJobWorkerOop::OnDidUseDefaultSettings(
+    SettingsCallback callback,
+    mojom::PrintSettingsResultPtr print_settings) {
+  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
+  mojom::ResultCode result;
+  if (print_settings->is_result_code()) {
+    result = print_settings->get_result_code();
+    DCHECK_NE(result, mojom::ResultCode::kSuccess);
+    PRINTER_LOG(ERROR) << "Error trying to use default settings: " << result;
+
+    // TODO(crbug.com/809738)  Fill in support for handling of access-denied
+    // result code.  Blocked on crbug.com/1243873 for Windows.
+  } else {
+    VLOG(1) << "Use default settings from service complete";
+    result = mojom::ResultCode::kSuccess;
+    printing_context()->ApplyPrintSettings(print_settings->get_settings());
+  }
+
+  GetSettingsDone(std::move(callback), result);
+}
+
+#if BUILDFLAG(IS_WIN)
+void PrintJobWorkerOop::OnDidAskUserForSettings(
+    SettingsCallback callback,
+    mojom::PrintSettingsResultPtr print_settings) {
+  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
+  mojom::ResultCode result;
+  if (print_settings->is_result_code()) {
+    result = print_settings->get_result_code();
+    DCHECK_NE(result, mojom::ResultCode::kSuccess);
+    if (result != mojom::ResultCode::kCanceled) {
+      PRINTER_LOG(ERROR) << "Error getting settings from user: " << result;
+    }
+
+    // TODO(crbug.com/809738)  Fill in support for handling of access-denied
+    // result code.  Blocked on crbug.com/1243873 for Windows.
+  } else {
+    VLOG(1) << "Ask user for settings from service complete";
+    result = mojom::ResultCode::kSuccess;
+    printing_context()->ApplyPrintSettings(print_settings->get_settings());
+  }
+
+  GetSettingsDone(std::move(callback), result);
+}
+#endif  // BUILDFLAG(IS_WIN)
+
 void PrintJobWorkerOop::OnDidStartPrinting(mojom::ResultCode result) {
   DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
   if (result != mojom::ResultCode::kSuccess) {
@@ -193,6 +240,38 @@
   // PrintBackend service.
 }
 
+void PrintJobWorkerOop::InvokeUseDefaultSettings(SettingsCallback callback) {
+  content::GetUIThreadTaskRunner({})->PostTask(
+      FROM_HERE,
+      base::BindOnce(&PrintJobWorkerOop::SendUseDefaultSettings,
+                     ui_weak_factory_.GetWeakPtr(), std::move(callback)));
+}
+
+void PrintJobWorkerOop::InvokeGetSettingsWithUI(uint32_t document_page_count,
+                                                bool has_selection,
+                                                bool is_scripted,
+                                                SettingsCallback callback) {
+#if BUILDFLAG(IS_WIN)
+  content::GetUIThreadTaskRunner({})->PostTask(
+      FROM_HERE,
+      base::BindOnce(&PrintJobWorkerOop::SendAskUserForSettings,
+                     ui_weak_factory_.GetWeakPtr(), document_page_count,
+                     has_selection, is_scripted, std::move(callback)));
+#else
+  // Invoke the browser version of getting settings with the system UI:
+  //   - macOS:  It is impossible to invoke a system dialog UI from a service
+  //       utility and have that dialog be application modal for a window that
+  //       was launched by the browser process.
+  //   - Linux:  TODO(crbug.com/809738)  Determine if Linux Wayland can be made
+  //       to have a system dialog be modal against an application window in the
+  //       browser process.
+  //   - Other platforms don't have a system print UI or do not use OOP
+  //     printing, so this does not matter.
+  PrintJobWorker::InvokeGetSettingsWithUI(document_page_count, has_selection,
+                                          is_scripted, std::move(callback));
+#endif
+}
+
 void PrintJobWorkerOop::UpdatePrintSettings(base::Value new_settings,
                                             SettingsCallback callback) {
   DCHECK_CURRENTLY_ON(BrowserThread::UI);
@@ -292,8 +371,8 @@
   if (print_settings->is_result_code()) {
     result = print_settings->get_result_code();
     DCHECK_NE(result, mojom::ResultCode::kSuccess);
-    PRINTER_LOG(ERROR) << "Failure to update print settings for " << device_name
-                       << " - error " << result;
+    PRINTER_LOG(ERROR) << "Error updating print settings for `" << device_name
+                       << "`: " << result;
 
     // TODO(crbug.com/809738)  Fill in support for handling of access-denied
     // result code.
@@ -306,6 +385,56 @@
   GetSettingsDone(std::move(callback), result);
 }
 
+void PrintJobWorkerOop::SendUseDefaultSettings(SettingsCallback callback) {
+  DCHECK_CURRENTLY_ON(BrowserThread::UI);
+  DCHECK(features::kEnableOopPrintDriversJobPrint.Get());
+
+  PrintBackendServiceManager& service_mgr =
+      PrintBackendServiceManager::GetInstance();
+
+  service_mgr.UseDefaultSettings(
+      /*printer_name=*/std::string(),
+      base::BindOnce(&PrintJobWorkerOop::OnDidUseDefaultSettings,
+                     ui_weak_factory_.GetWeakPtr(), std::move(callback)));
+}
+
+#if BUILDFLAG(IS_WIN)
+void PrintJobWorkerOop::SendAskUserForSettings(uint32_t document_page_count,
+                                               bool has_selection,
+                                               bool is_scripted,
+                                               SettingsCallback callback) {
+  DCHECK_CURRENTLY_ON(BrowserThread::UI);
+  DCHECK(features::kEnableOopPrintDriversJobPrint.Get());
+
+  if (document_page_count > kMaxPageCount) {
+    GetSettingsDone(std::move(callback), mojom::ResultCode::kFailed);
+    return;
+  }
+
+  // Save the print target type from the settings, since this will be needed
+  // later when printing is started.
+  print_target_type_ = mojom::PrintTargetType::kDirectToDevice;
+
+  content::WebContents* web_contents = GetWebContents();
+
+  // Running a dialog causes an exit to webpage-initiated fullscreen.
+  // http://crbug.com/728276
+  if (web_contents && web_contents->IsFullscreen())
+    web_contents->ExitFullscreen(true);
+
+  gfx::NativeView parent_view =
+      web_contents ? web_contents->GetTopLevelNativeWindow() : nullptr;
+
+  PrintBackendServiceManager& service_mgr =
+      PrintBackendServiceManager::GetInstance();
+  service_mgr.AskUserForSettings(
+      /*printer_name=*/std::string(), parent_view, document_page_count,
+      has_selection, is_scripted,
+      base::BindOnce(&PrintJobWorkerOop::OnDidAskUserForSettings,
+                     ui_weak_factory_.GetWeakPtr(), std::move(callback)));
+}
+#endif  // BUILDFLAG(IS_WIN)
+
 void PrintJobWorkerOop::SendStartPrinting(const std::string& device_name,
                                           const std::u16string& document_name) {
   DCHECK_CURRENTLY_ON(BrowserThread::UI);
diff --git a/chrome/browser/printing/print_job_worker_oop.h b/chrome/browser/printing/print_job_worker_oop.h
index 20d822f..12850b2 100644
--- a/chrome/browser/printing/print_job_worker_oop.h
+++ b/chrome/browser/printing/print_job_worker_oop.h
@@ -42,6 +42,14 @@
  protected:
   // Local callback wrappers for Print Backend Service mojom call.  Virtual to
   // support testing.
+  virtual void OnDidUseDefaultSettings(
+      SettingsCallback callback,
+      mojom::PrintSettingsResultPtr print_settings);
+#if BUILDFLAG(IS_WIN)
+  virtual void OnDidAskUserForSettings(
+      SettingsCallback callback,
+      mojom::PrintSettingsResultPtr print_settings);
+#endif
   virtual void OnDidStartPrinting(mojom::ResultCode result);
 #if BUILDFLAG(IS_WIN)
   virtual void OnDidRenderPrintedPage(uint32_t page_index,
@@ -54,6 +62,11 @@
   void SpoolPage(PrintedPage* page) override;
 #endif
   void OnDocumentDone() override;
+  void InvokeUseDefaultSettings(SettingsCallback callback) override;
+  void InvokeGetSettingsWithUI(uint32_t document_page_count,
+                               bool has_selection,
+                               bool is_scripted,
+                               SettingsCallback callback) override;
   void UpdatePrintSettings(base::Value new_settings,
                            SettingsCallback callback) override;
   void OnFailure() override;
@@ -78,6 +91,13 @@
                                 mojom::PrintSettingsResultPtr print_settings);
 
   // Mojo support to send messages from UI thread.
+  void SendUseDefaultSettings(SettingsCallback callback);
+#if BUILDFLAG(IS_WIN)
+  void SendAskUserForSettings(uint32_t document_page_count,
+                              bool has_selection,
+                              bool is_scripted,
+                              SettingsCallback callback);
+#endif
   void SendStartPrinting(const std::string& device_name,
                          const std::u16string& document_name);
 #if BUILDFLAG(IS_WIN)
diff --git a/chrome/browser/printing/print_view_manager.cc b/chrome/browser/printing/print_view_manager.cc
index 8b292d4..bba6dbc 100644
--- a/chrome/browser/printing/print_view_manager.cc
+++ b/chrome/browser/printing/print_view_manager.cc
@@ -106,8 +106,12 @@
     return false;
   }
 
-  // TODO(crbug.com/809738)  Register with `PrintBackendServiceManager` when
-  // system print is enabled out-of-process.
+#if BUILDFLAG(ENABLE_OOP_PRINTING)
+  // Register this worker so that the service persists as long as the user
+  // keeps the system print dialog UI displayed.
+  if (!RegisterSystemPrintClient())
+    return false;
+#endif
 
   SetPrintingRFH(print_preview_rfh_);
   GetPrintRenderFrame(print_preview_rfh_)->PrintForSystemDialog();
diff --git a/chrome/browser/printing/print_view_manager_base.cc b/chrome/browser/printing/print_view_manager_base.cc
index 8765dc3c..818eee6 100644
--- a/chrome/browser/printing/print_view_manager_base.cc
+++ b/chrome/browser/printing/print_view_manager_base.cc
@@ -55,13 +55,10 @@
 #include "printing/mojom/print.mojom.h"
 #include "printing/print_settings.h"
 #include "printing/printed_document.h"
+#include "printing/printing_features.h"
 #include "printing/printing_utils.h"
 #include "ui/base/l10n/l10n_util.h"
 
-#if BUILDFLAG(IS_WIN)
-#include "printing/printing_features.h"
-#endif
-
 #if !BUILDFLAG(IS_ANDROID)
 #include "chrome/browser/printing/print_error_dialog.h"
 #endif
@@ -71,6 +68,10 @@
 #include "components/prefs/pref_service.h"
 #endif
 
+#if BUILDFLAG(ENABLE_OOP_PRINTING)
+#include "chrome/browser/printing/print_backend_service_manager.h"
+#endif
+
 #if BUILDFLAG(IS_WIN) && BUILDFLAG(GOOGLE_CHROME_BRANDING)
 #include "chrome/browser/win/conflicts/module_database.h"
 #endif
@@ -324,9 +325,12 @@
   if (!content::RenderFrameHost::FromID(rfh_id) || !rfh->IsRenderFrameLive())
     return false;
 
-  // TODO(crbug.com/809738)  Register with `PrintBackendServiceManager` when
-  // system print is enabled out-of-process.  A corresponding unregister should
-  // go in `ReleasePrintJob()`.
+#if BUILDFLAG(ENABLE_OOP_PRINTING)
+  // Register this worker so that the service persists as long as the user
+  // keeps the system print dialog UI displayed.
+  if (!RegisterSystemPrintClient())
+    return false;
+#endif
 
   SetPrintingRFH(rfh);
   GetPrintRenderFrame(rfh)->PrintRequestedPages();
@@ -465,6 +469,16 @@
     GetDefaultPrintSettingsCallback callback,
     mojom::PrintParamsPtr params) {
   DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
+#if BUILDFLAG(ENABLE_OOP_PRINTING)
+  if (printing::features::kEnableOopPrintDriversJobPrint.Get() &&
+      !params->document_cookie) {
+    // The attempt to use the default settings failed.  There should be no
+    // subsequent call to get settings from the user that would normally be
+    // shared as part of this client registration.  Immediately notify the
+    // service manager that this client is no longer needed.
+    UnregisterSystemPrintClient();
+  }
+#endif
   set_cookie(params->document_cookie);
   std::move(callback).Run(std::move(params));
 }
@@ -475,6 +489,11 @@
     mojom::PrintPagesParamsPtr params) {
   DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
 
+#if BUILDFLAG(ENABLE_OOP_PRINTING)
+  // Finished getting all settings (defaults and from user), no further need
+  // to be registered as a system print client.
+  UnregisterSystemPrintClient();
+#endif
   if (!content::RenderProcessHost::FromID(process_id)) {
     // Early return if the renderer is not alive.
     return;
@@ -610,6 +629,15 @@
                                  mojom::PrintParams::New());
     return;
   }
+#if BUILDFLAG(ENABLE_OOP_PRINTING)
+  if (printing::features::kEnableOopPrintDriversJobPrint.Get() &&
+      !service_manager_client_id_.has_value()) {
+    // Renderer process has requested settings outside of the expected setup.
+    GetDefaultPrintSettingsReply(std::move(callback),
+                                 mojom::PrintParams::New());
+    return;
+  }
+#endif
 
   content::RenderFrameHost* render_frame_host = GetCurrentTargetFrame();
   auto callback_wrapper =
@@ -676,6 +704,14 @@
     std::move(callback).Run(CreateEmptyPrintPagesParamsPtr());
     return;
   }
+#if BUILDFLAG(ENABLE_OOP_PRINTING)
+  if (printing::features::kEnableOopPrintDriversJobPrint.Get() &&
+      !service_manager_client_id_.has_value()) {
+    // Renderer process has requested settings outside of the expected setup.
+    std::move(callback).Run(CreateEmptyPrintPagesParamsPtr());
+    return;
+  }
+#endif
   auto callback_wrapper = base::BindOnce(
       &PrintViewManagerBase::ScriptedPrintReply, weak_ptr_factory_.GetWeakPtr(),
       std::move(callback), render_process_host->GetID());
@@ -918,6 +954,13 @@
   content::RenderFrameHost* rfh = printing_rfh_;
   printing_rfh_ = nullptr;
 
+#if BUILDFLAG(ENABLE_OOP_PRINTING)
+  // Ensure that any residual registration of printing client is released.
+  // This might be necessary in some abnormal cases, such as the associated
+  // render process having terminated.
+  UnregisterSystemPrintClient();
+#endif
+
   if (!print_job_)
     return;
 
@@ -1020,6 +1063,31 @@
   printing_rfh_ = rfh;
 }
 
+#if BUILDFLAG(ENABLE_OOP_PRINTING)
+bool PrintViewManagerBase::RegisterSystemPrintClient() {
+  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
+  DCHECK(!service_manager_client_id_.has_value());
+  service_manager_client_id_ =
+      PrintBackendServiceManager::GetInstance().RegisterQueryWithUiClient();
+  if (!service_manager_client_id_.has_value()) {
+    DVLOG(1) << "Multiple system print clients not allowed, skipping user "
+                "request.";
+    return false;
+  }
+  return true;
+}
+
+void PrintViewManagerBase::UnregisterSystemPrintClient() {
+  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
+  if (!service_manager_client_id_.has_value())
+    return;
+
+  PrintBackendServiceManager::GetInstance().UnregisterClient(
+      *service_manager_client_id_);
+  service_manager_client_id_.reset();
+}
+#endif  // BUILDFLAG(ENABLE_OOP_PRINTING)
+
 void PrintViewManagerBase::ReleasePrinterQuery() {
   int current_cookie = cookie();
   if (!current_cookie)
diff --git a/chrome/browser/printing/print_view_manager_base.h b/chrome/browser/printing/print_view_manager_base.h
index 2661776..3a4cfa1 100644
--- a/chrome/browser/printing/print_view_manager_base.h
+++ b/chrome/browser/printing/print_view_manager_base.h
@@ -121,6 +121,20 @@
 
   void SetPrintingRFH(content::RenderFrameHost* rfh);
 
+#if BUILDFLAG(ENABLE_OOP_PRINTING)
+  // Register with the `PrintBackendServiceManager` as a client for queries
+  // which will require a UI (the system print dialog).  Some platforms have
+  // limitations on having multiple clients of this type; this function returns
+  // `false` if such a registration fails because of this restriction.  In
+  // that case no further attempts to make the queries should be made.
+  bool RegisterSystemPrintClient();
+
+  // Unregister with the `PrintBackendServiceManager` if a client for queries
+  // which require a UI.  This function can be called even if there is no
+  // current registration.
+  void UnregisterSystemPrintClient();
+#endif
+
   // content::WebContentsObserver implementation.
   void RenderFrameDeleted(content::RenderFrameHost* render_frame_host) override;
 
@@ -259,6 +273,11 @@
   // Whether printing is enabled.
   BooleanPrefMember printing_enabled_;
 
+#if BUILDFLAG(ENABLE_OOP_PRINTING)
+  // Client ID with the print backend service manager for this print job.
+  absl::optional<uint32_t> service_manager_client_id_;
+#endif
+
   const scoped_refptr<PrintQueriesQueue> queue_;
 
   base::ObserverList<Observer> observers_;
diff --git a/chrome/browser/profiles/profile.cc b/chrome/browser/profiles/profile.cc
index 1e10a87..c34520b 100644
--- a/chrome/browser/profiles/profile.cc
+++ b/chrome/browser/profiles/profile.cc
@@ -26,7 +26,6 @@
 #include "components/keyed_service/content/browser_context_dependency_manager.h"
 #include "components/language/core/browser/pref_names.h"
 #include "components/live_caption/pref_names.h"
-#include "components/media_router/common/pref_names.h"
 #include "components/pref_registry/pref_registry_syncable.h"
 #include "components/profile_metrics/browser_profile_type.h"
 #include "components/variations/variations.mojom.h"
@@ -351,13 +350,6 @@
                                std::string());
 #endif
 
-#if !BUILDFLAG(IS_ANDROID)
-  registry->RegisterBooleanPref(
-      media_router::prefs::kMediaRouterMediaRemotingEnabled, true);
-  registry->RegisterListPref(
-      media_router::prefs::kMediaRouterTabMirroringSources);
-#endif
-
   registry->RegisterDictionaryPref(prefs::kWebShareVisitedTargets);
   registry->RegisterDictionaryPref(
       prefs::kProtocolHandlerPerOriginAllowedProtocols);
diff --git a/chrome/browser/resources/chromeos/accessibility/accessibility_common/autoclick/autoclick_test.js b/chrome/browser/resources/chromeos/accessibility/accessibility_common/autoclick/autoclick_test.js
index fdb885e..7904fbb 100644
--- a/chrome/browser/resources/chromeos/accessibility/accessibility_common/autoclick/autoclick_test.js
+++ b/chrome/browser/resources/chromeos/accessibility/accessibility_common/autoclick/autoclick_test.js
@@ -70,84 +70,81 @@
   }
 };
 
-TEST_F('AutoclickE2ETest', 'HighlightsRootWebAreaIfNotScrollable', function() {
-  this.runWithLoadedTree(
-      'data:text/html;charset=utf-8,<p>Cats rock!</p>', async function(root) {
-        const node = root.find(
-            {role: RoleType.STATIC_TEXT, attributes: {name: 'Cats rock!'}});
-        await new Promise(resolve => {
-          this.mockAccessibilityPrivate.callOnScrollableBoundsForPointRequested(
-              // Offset slightly into the node to ensure the hittest
-              // happens within the node.
-              node.location.left + 1, node.location.top + 1, resolve);
-        });
-        const expected = node.root.location;
-        const focusRings = this.mockAccessibilityPrivate.getFocusRings();
-        this.assertSameRect(
-            this.mockAccessibilityPrivate.getScrollableBounds(), expected);
-        this.assertSameRect(focusRings[0].rects[0], expected);
+TEST_F(
+    'AutoclickE2ETest', 'HighlightsRootWebAreaIfNotScrollable',
+    async function() {
+      const root = await this.runWithLoadedTree(
+          'data:text/html;charset=utf-8,<p>Cats rock!</p>');
+      const node = root.find(
+          {role: RoleType.STATIC_TEXT, attributes: {name: 'Cats rock!'}});
+      await new Promise(resolve => {
+        this.mockAccessibilityPrivate.callOnScrollableBoundsForPointRequested(
+            // Offset slightly into the node to ensure the hittest
+            // happens within the node.
+            node.location.left + 1, node.location.top + 1, resolve);
       });
-});
+      const expected = node.root.location;
+      const focusRings = this.mockAccessibilityPrivate.getFocusRings();
+      this.assertSameRect(
+          this.mockAccessibilityPrivate.getScrollableBounds(), expected);
+      this.assertSameRect(focusRings[0].rects[0], expected);
+    });
 
-TEST_F('AutoclickE2ETest', 'HighlightsScrollableDiv', function() {
-  this.runWithLoadedTree(
+TEST_F('AutoclickE2ETest', 'HighlightsScrollableDiv', async function() {
+  const root = await this.runWithLoadedTree(
       'data:text/html;charset=utf-8,' +
-          '<div style="width:100px;height:100px;overflow:scroll">' +
-          '<div style="margin:50px">cats rock! this text wraps and overflows!' +
-          '</div></div>',
-      async function(root) {
-        const node = root.find({
-          role: RoleType.STATIC_TEXT,
-          attributes: {name: 'cats rock! this text wraps and overflows!'}
-        });
-        await new Promise(resolve => {
-          this.mockAccessibilityPrivate.callOnScrollableBoundsForPointRequested(
-              // Offset slightly into the node to ensure the hittest happens
-              // within the node.
-              node.location.left + 1, node.location.top + 1, resolve);
-        });
-        // The outer div, which is the parent of the parent of the
-        // text, is scrollable.
-        assertTrue(node.parent.parent.scrollable);
-        const expected = node.parent.parent.location;
-        const focusRings = this.mockAccessibilityPrivate.getFocusRings();
-        this.assertSameRect(
-            this.mockAccessibilityPrivate.getScrollableBounds(), expected);
-        this.assertSameRect(focusRings[0].rects[0], expected);
-      });
+      '<div style="width:100px;height:100px;overflow:scroll">' +
+      '<div style="margin:50px">cats rock! this text wraps and overflows!' +
+      '</div></div>');
+  const node = root.find({
+    role: RoleType.STATIC_TEXT,
+    attributes: {name: 'cats rock! this text wraps and overflows!'}
+  });
+  await new Promise(resolve => {
+    this.mockAccessibilityPrivate.callOnScrollableBoundsForPointRequested(
+        // Offset slightly into the node to ensure the hittest happens
+        // within the node.
+        node.location.left + 1, node.location.top + 1, resolve);
+  });
+  // The outer div, which is the parent of the parent of the
+  // text, is scrollable.
+  assertTrue(node.parent.parent.scrollable);
+  const expected = node.parent.parent.location;
+  const focusRings = this.mockAccessibilityPrivate.getFocusRings();
+  this.assertSameRect(
+      this.mockAccessibilityPrivate.getScrollableBounds(), expected);
+  this.assertSameRect(focusRings[0].rects[0], expected);
 });
 
-TEST_F('AutoclickE2ETest', 'RemovesAndAddsAutoclick', function() {
-  this.runWithLoadedTree(
-      'data:text/html;charset=utf-8,<p>Cats rock!</p>', async function(root) {
-        // Turn on screen magnifier so that when we turn off autoclick, the
-        // extension doesn't get unloaded and crash the test.
-        await new Promise(resolve => {
-          chrome.accessibilityFeatures.screenMagnifier.set(
-              {value: true}, resolve);
-        });
+TEST_F('AutoclickE2ETest', 'RemovesAndAddsAutoclick', async function() {
+  const root = await this.runWithLoadedTree(
+      'data:text/html;charset=utf-8,<p>Cats rock!</p>');
+  // Turn on screen magnifier so that when we turn off autoclick, the
+  // extension doesn't get unloaded and crash the test.
+  await new Promise(resolve => {
+    chrome.accessibilityFeatures.screenMagnifier.set({value: true}, resolve);
+  });
 
-        // Toggle autoclick off and on, ensure it still works and no crashes.
-        await new Promise(resolve => {
-          chrome.accessibilityFeatures.autoclick.set({value: false}, resolve);
-        });
-        await new Promise(resolve => {
-          chrome.accessibilityFeatures.autoclick.set({value: true}, resolve);
-        });
-        const node = root.find(
-            {role: RoleType.STATIC_TEXT, attributes: {name: 'Cats rock!'}});
-        await new Promise(resolve => {
-          this.mockAccessibilityPrivate.callOnScrollableBoundsForPointRequested(
-              // Offset slightly into the node to ensure the hittest happens
-              // within the node.
-              node.location.left + 1, node.location.top + 1, resolve);
-        });
-        const expected = node.root.location;
-        const focusRings = this.mockAccessibilityPrivate.getFocusRings();
-        this.assertSameRect(
-            this.mockAccessibilityPrivate.getScrollableBounds(), expected);
-        this.assertSameRect(focusRings[0].rects[0], expected);
-      });
+  // Toggle autoclick off and on, ensure it still works and no crashes.
+  await new Promise(resolve => {
+    chrome.accessibilityFeatures.autoclick.set({value: false}, resolve);
+  });
+  await new Promise(resolve => {
+    chrome.accessibilityFeatures.autoclick.set({value: true}, resolve);
+  });
+  const node =
+      root.find({role: RoleType.STATIC_TEXT, attributes: {name: 'Cats rock!'}});
+  await new Promise(resolve => {
+    this.mockAccessibilityPrivate.callOnScrollableBoundsForPointRequested(
+        // Offset slightly into the node to ensure the hittest happens
+        // within the node.
+        node.location.left + 1, node.location.top + 1, resolve);
+  });
+  const expected = node.root.location;
+  const focusRings = this.mockAccessibilityPrivate.getFocusRings();
+  this.assertSameRect(
+      this.mockAccessibilityPrivate.getScrollableBounds(), expected);
+  this.assertSameRect(focusRings[0].rects[0], expected);
 });
 
 // TODO(crbug.com/978163): Add tests for when the scrollable area is scrolled
diff --git a/chrome/browser/resources/chromeos/accessibility/accessibility_common/magnifier/magnifier_test.js b/chrome/browser/resources/chromeos/accessibility/accessibility_common/magnifier/magnifier_test.js
index 8e48f48..159f2cfc 100644
--- a/chrome/browser/resources/chromeos/accessibility/accessibility_common/magnifier/magnifier_test.js
+++ b/chrome/browser/resources/chromeos/accessibility/accessibility_common/magnifier/magnifier_test.js
@@ -66,39 +66,39 @@
 // Flaky: http://crbug.com/1171635
 TEST_F(
     'MagnifierE2ETest', 'DISABLED_MovesScreenMagnifierToFocusedElement',
-    function() {
+    async function() {
       const site = `
         <button id="apple">Apple</button><br />
         <button id="banana" style="margin-top: 400px">Banana</button>
       `;
-      this.runWithLoadedTree(site, async function(root) {
-        const magnifier = accessibilityCommon.getMagnifierForTest();
-        magnifier.setIsInitializingForTest(false);
+      const root = await this.runWithLoadedTree(site);
+      const magnifier = accessibilityCommon.getMagnifierForTest();
+      magnifier.setIsInitializingForTest(false);
 
-        const apple = root.find({attributes: {name: 'Apple'}});
-        const banana = root.find({attributes: {name: 'Banana'}});
+      const apple = root.find({attributes: {name: 'Apple'}});
+      const banana = root.find({attributes: {name: 'Banana'}});
 
-        // Focus and move magnifier to apple.
-        apple.focus();
+      // Focus and move magnifier to apple.
+      apple.focus();
 
-        // Verify magnifier bounds contains apple, but not banana.
-        let bounds = await this.getNextMagnifierBounds();
-        assertTrue(RectUtil.contains(bounds, apple.location));
-        assertFalse(RectUtil.contains(bounds, banana.location));
+      // Verify magnifier bounds contains apple, but not banana.
+      let bounds = await this.getNextMagnifierBounds();
+      assertTrue(RectUtil.contains(bounds, apple.location));
+      assertFalse(RectUtil.contains(bounds, banana.location));
 
-        // Focus and move magnifier to banana.
-        banana.focus();
+      // Focus and move magnifier to banana.
+      banana.focus();
 
-        // Verify magnifier bounds contains banana, but not apple.
-        bounds = await this.getNextMagnifierBounds();
-        assertFalse(RectUtil.contains(bounds, apple.location));
-        assertTrue(RectUtil.contains(bounds, banana.location));
-      });
+      // Verify magnifier bounds contains banana, but not apple.
+      bounds = await this.getNextMagnifierBounds();
+      assertFalse(RectUtil.contains(bounds, apple.location));
+      assertTrue(RectUtil.contains(bounds, banana.location));
     });
 
 // Disabled - flaky: https://crbug.com/1145612
 TEST_F(
-    'MagnifierE2ETest', 'DISABLED_MovesDockedMagnifierToActiveDescendant', function() {
+    'MagnifierE2ETest', 'DISABLED_MovesDockedMagnifierToActiveDescendant',
+    async function() {
       const site = `
     <div role="group" id="parent" aria-activedescendant="apple">
       <div id="apple" role="treeitem">Apple</div>
@@ -111,36 +111,35 @@
       });
       </script>
   `;
-      this.runWithLoadedTree(site, async function(root) {
-        // Enable docked magnifier.
-        await new Promise(resolve => {
-          chrome.accessibilityFeatures.dockedMagnifier.set(
-              {value: true}, resolve);
-        });
+      const root = await this.runWithLoadedTree(site);
+      // Enable docked magnifier.
+      await new Promise(resolve => {
+        chrome.accessibilityFeatures.dockedMagnifier.set(
+            {value: true}, resolve);
+      });
 
-        // Validate magnifier wants to move to root.
-        const rootLocation = await getNextMagnifierLocation();
-        assertTrue(RectUtil.equal(rootLocation, root.location));
+      // Validate magnifier wants to move to root.
+      const rootLocation = await getNextMagnifierLocation();
+      assertTrue(RectUtil.equal(rootLocation, root.location));
 
-        // Click parent to change active descendant from apple to banana.
-        const parent = root.find({role: RoleType.GROUP});
-        parent.doDefault();
+      // Click parent to change active descendant from apple to banana.
+      const parent = root.find({role: RoleType.GROUP});
+      parent.doDefault();
 
-        // Register and wait for rect from magnifier.
-        const rect = await getNextMagnifierLocation();
+      // Register and wait for rect from magnifier.
+      const rect = await getNextMagnifierLocation();
 
-        // Validate rect from magnifier is rect of banana.
-        const bananaNode =
-            root.find({role: RoleType.TREE_ITEM, attributes: {name: 'Banana'}});
-        assertTrue(RectUtil.equal(rect, bananaNode.location));
-      }, {returnPage: true});
+      // Validate rect from magnifier is rect of banana.
+      const bananaNode =
+          root.find({role: RoleType.TREE_ITEM, attributes: {name: 'Banana'}});
+      assertTrue(RectUtil.equal(rect, bananaNode.location));
     });
 
 
 // Flaky: http://crbug.com/1171750
 TEST_F(
     'MagnifierE2ETest', 'DISABLED_MovesScreenMagnifierToActiveDescendant',
-    function() {
+    async function() {
       const site = `
     <span tabindex="1">Top</span>
     <div id="group" role="group" style="width: 200px"
@@ -155,28 +154,27 @@
       });
     </script>
   `;
-      this.runWithLoadedTree(site, async function(root) {
-        const magnifier = accessibilityCommon.getMagnifierForTest();
-        magnifier.setIsInitializingForTest(false);
+      const root = await this.runWithLoadedTree(site);
+      const magnifier = accessibilityCommon.getMagnifierForTest();
+      magnifier.setIsInitializingForTest(false);
 
-        const top = root.find({attributes: {name: 'Top'}});
-        const banana = root.find({attributes: {name: 'Banana'}});
-        const group = root.find({role: RoleType.GROUP});
+      const top = root.find({attributes: {name: 'Top'}});
+      const banana = root.find({attributes: {name: 'Banana'}});
+      const group = root.find({role: RoleType.GROUP});
 
-        // Focus and move magnifier to top.
-        top.focus();
+      // Focus and move magnifier to top.
+      top.focus();
 
-        // Verify magnifier bounds don't contain banana.
-        let bounds = await this.getNextMagnifierBounds();
-        assertFalse(RectUtil.contains(bounds, banana.location));
+      // Verify magnifier bounds don't contain banana.
+      let bounds = await this.getNextMagnifierBounds();
+      assertFalse(RectUtil.contains(bounds, banana.location));
 
-        // Click group to change active descendant to banana.
-        group.doDefault();
+      // Click group to change active descendant to banana.
+      group.doDefault();
 
-        // Verify magnifier bounds contain banana.
-        bounds = await this.getNextMagnifierBounds();
-        assertTrue(RectUtil.contains(bounds, banana.location));
-      });
+      // Verify magnifier bounds contain banana.
+      bounds = await this.getNextMagnifierBounds();
+      assertTrue(RectUtil.contains(bounds, banana.location));
     });
 
 TEST_F(
@@ -225,62 +223,60 @@
       });
     });
 
-TEST_F('MagnifierE2ETest', 'IgnoresRootNodeFocus', function() {
+TEST_F('MagnifierE2ETest', 'IgnoresRootNodeFocus', async function() {
   const magnifier = accessibilityCommon.getMagnifierForTest();
   magnifier.setIsInitializingForTest(false);
 
-  this.runWithLoadedTree('', async function(root) {
-    chrome.accessibilityPrivate.onMagnifierBoundsChanged.addListener(
-        newBounds => {
-          throw new Error(
-              'Magnifier did not ignore focus change on document load - ' +
-              'moved to following location: ' + JSON.stringify(newBounds));
-        });
+  await this.runWithLoadedTree('');
+  chrome.accessibilityPrivate.onMagnifierBoundsChanged.addListener(
+      newBounds => {
+        throw new Error(
+            'Magnifier did not ignore focus change on document load - ' +
+            'moved to following location: ' + JSON.stringify(newBounds));
+      });
 
-    // Wait seven seconds to verify magnifier successfully ignored focus on root
-    // node.
-    await new Promise(resolve => setTimeout(resolve, 7000));
-  });
+  // Wait seven seconds to verify magnifier successfully ignored focus on root
+  // node.
+  await new Promise(resolve => setTimeout(resolve, 7000));
 });
 
 // TODO(crbug.com/1295685): Test is flaky.
-TEST_F('MagnifierE2ETest', 'DISABLED_MagnifierCenterOnPoint', function() {
-  this.runWithLoadedTree('', async function(root) {
-    const magnifier = accessibilityCommon.getMagnifierForTest();
-    magnifier.setIsInitializingForTest(false);
+TEST_F('MagnifierE2ETest', 'DISABLED_MagnifierCenterOnPoint', async function() {
+  await this.runWithLoadedTree('');
+  const magnifier = accessibilityCommon.getMagnifierForTest();
+  magnifier.setIsInitializingForTest(false);
 
-    const movePointAssertBounds = async (targetPoint, targetBounds) => {
-      // Repeatedly center magnifier on |targetPoint|.
-      const id = setInterval(() => {
-        chrome.accessibilityPrivate.magnifierCenterOnPoint(targetPoint);
-      }, 500);
+  const movePointAssertBounds = async (targetPoint, targetBounds) => {
+    // Repeatedly center magnifier on |targetPoint|.
+    const id = setInterval(() => {
+      chrome.accessibilityPrivate.magnifierCenterOnPoint(targetPoint);
+    }, 500);
 
-      // Verify new magnifier bounds include |targetBounds|.
-      await new Promise(resolve => {
-        const boundsChangedListener = newBounds => {
-          if (RectUtil.contains(newBounds, targetBounds)) {
-            chrome.accessibilityPrivate.onMagnifierBoundsChanged.removeListener(
-                boundsChangedListener);
-            clearInterval(id);
-            resolve();
-          }
-        };
-        chrome.accessibilityPrivate.onMagnifierBoundsChanged.addListener(
-            boundsChangedListener);
-      });
-    };
+    // Verify new magnifier bounds include |targetBounds|.
+    await new Promise(resolve => {
+      const boundsChangedListener = newBounds => {
+        if (RectUtil.contains(newBounds, targetBounds)) {
+          chrome.accessibilityPrivate.onMagnifierBoundsChanged.removeListener(
+              boundsChangedListener);
+          clearInterval(id);
+          resolve();
+        }
+      };
+      chrome.accessibilityPrivate.onMagnifierBoundsChanged.addListener(
+          boundsChangedListener);
+    });
+  };
 
-    // Move magnifier to upper left of screen.
-    await movePointAssertBounds(
-        {x: 100, y: 100}, {left: 100, top: 100, width: 0, height: 0});
+  // Move magnifier to upper left of screen.
+  await movePointAssertBounds(
+      {x: 100, y: 100}, {left: 100, top: 100, width: 0, height: 0});
 
-    // Move magnifier to lower right of screen.
-    await movePointAssertBounds(
-        {x: 650, y: 450}, {left: 650, top: 450, width: 0, height: 0});
-  });
+  // Move magnifier to lower right of screen.
+  await movePointAssertBounds(
+      {x: 650, y: 450}, {left: 650, top: 450, width: 0, height: 0});
 });
 
-TEST_F('MagnifierE2ETest', 'OnCaretBoundsChanged', function() {
+TEST_F('MagnifierE2ETest', 'OnCaretBoundsChanged', async function() {
   const site = `
     <input type="text" id="input" style="width: 1000px">
     <button id="button">Type words</button>
@@ -293,43 +289,42 @@
       });
     </script>
   `;
-  this.runWithLoadedTree(site, async function(root) {
-    const magnifier = accessibilityCommon.getMagnifierForTest();
-    magnifier.setIsInitializingForTest(false);
-    const button = root.find({attributes: {name: 'Type words'}});
-    const input = root.find({role: RoleType.TEXT_FIELD});
-    input.doDefault();
+  const root = await this.runWithLoadedTree(site);
+  const magnifier = accessibilityCommon.getMagnifierForTest();
+  magnifier.setIsInitializingForTest(false);
+  const button = root.find({attributes: {name: 'Type words'}});
+  const input = root.find({role: RoleType.TEXT_FIELD});
+  input.doDefault();
 
-    const typeWordsAssertBounds = async targetBounds => {
-      // Type words in the input field to move the text caret forward.
-      const id = setInterval(() => {
-        button.doDefault();
-      }, 500);
+  const typeWordsAssertBounds = async targetBounds => {
+    // Type words in the input field to move the text caret forward.
+    const id = setInterval(() => {
+      button.doDefault();
+    }, 500);
 
-      // Verify new magnifier bounds include |targetBounds|.
-      await new Promise(resolve => {
-        const boundsChangedListener = newBounds => {
-          if (RectUtil.contains(newBounds, targetBounds)) {
-            chrome.accessibilityPrivate.onMagnifierBoundsChanged.removeListener(
-                boundsChangedListener);
-            clearInterval(id);
-            resolve();
-          }
-        };
-        chrome.accessibilityPrivate.onMagnifierBoundsChanged.addListener(
-            boundsChangedListener);
-      });
-    };
+    // Verify new magnifier bounds include |targetBounds|.
+    await new Promise(resolve => {
+      const boundsChangedListener = newBounds => {
+        if (RectUtil.contains(newBounds, targetBounds)) {
+          chrome.accessibilityPrivate.onMagnifierBoundsChanged.removeListener(
+              boundsChangedListener);
+          clearInterval(id);
+          resolve();
+        }
+      };
+      chrome.accessibilityPrivate.onMagnifierBoundsChanged.addListener(
+          boundsChangedListener);
+    });
+  };
 
-    // Type words to move text cursor forward, verify magnifier contains caret.
-    await typeWordsAssertBounds({left: 400, top: 100, width: 0, height: 0});
+  // Type words to move text cursor forward, verify magnifier contains caret.
+  await typeWordsAssertBounds({left: 400, top: 100, width: 0, height: 0});
 
-    // Additional words to move caret forward, make sure magnifier follows.
-    await typeWordsAssertBounds({left: 800, top: 100, width: 0, height: 0});
+  // Additional words to move caret forward, make sure magnifier follows.
+  await typeWordsAssertBounds({left: 800, top: 100, width: 0, height: 0});
 
-    // Even more words to move caret forward, make sure magnifier follows.
-    await typeWordsAssertBounds({left: 1200, top: 100, width: 0, height: 0});
-  });
+  // Even more words to move caret forward, make sure magnifier follows.
+  await typeWordsAssertBounds({left: 1200, top: 100, width: 0, height: 0});
 });
 
 
diff --git a/chrome/browser/resources/chromeos/accessibility/chromevox/BUILD.gn b/chrome/browser/resources/chromeos/accessibility/chromevox/BUILD.gn
index a3f5b0c..4d047674 100644
--- a/chrome/browser/resources/chromeos/accessibility/chromevox/BUILD.gn
+++ b/chrome/browser/resources/chromeos/accessibility/chromevox/BUILD.gn
@@ -35,7 +35,6 @@
   "background/chromevox_state.js",
   "background/command_handler_interface.js",
   "background/event_source.js",
-  "background/keymaps/key_map.js",
   "background/output/locale_output_helper.js",
   "background/logging/event_stream_logger.js",
   "background/logging/log_store.js",
@@ -64,10 +63,8 @@
   "braille/pan_strategy.js",
   "braille/spans.js",
   "common/abstract_earcons.js",
-  "common/abstract_tts.js",
   "common/braille_interface.js",
   "common/chromevox.js",
-  "common/command_store.js",
   "common/console_tts.js",
   "common/extension_bridge.js",
   "common/key_sequence.js",
@@ -110,6 +107,7 @@
   "background/gesture_interface.js",
   "background/injected_script_loader.js",
   "background/keyboard_handler.js",
+  "background/keymaps/key_map.js",
   "background/live_regions.js",
   "background/math_handler.js",
   "background/media_automation_handler.js",
@@ -120,6 +118,8 @@
   "background/smart_sticky_mode.js",
   "background/logging/log.js",
   "braille/braille_key_event_rewriter.js",
+  "common/abstract_tts.js",
+  "common/command_store.js",
   "common/composite_tts.js",
   "common/editable_text_base.js",
   "common/keyboard_handler.js",
@@ -382,7 +382,6 @@
       "braille/spans.js",
       "braille/liblouis.js",
       "common/abstract_earcons.js",
-      "common/abstract_tts.js",
       "common/braille_interface.js",
       "common/chromevox.js",
       "common/editable_text_base.js",
diff --git a/chrome/browser/resources/chromeos/accessibility/chromevox/background/auto_scroll_handler_test.js b/chrome/browser/resources/chromeos/accessibility/chromevox/background/auto_scroll_handler_test.js
index a91f483..301f602d 100644
--- a/chrome/browser/resources/chromeos/accessibility/chromevox/background/auto_scroll_handler_test.js
+++ b/chrome/browser/resources/chromeos/accessibility/chromevox/background/auto_scroll_handler_test.js
@@ -24,7 +24,8 @@
         'AutoScrollHandler', '/chromevox/background/auto_scroll_handler.js');
   }
 
-  runWithFakeArcSimpleScrollable(callback) {
+  /** @return {chrome.automation.AutomationNode} */
+  async runWithFakeArcSimpleScrollable() {
     // This simulates a scrolling behavior of Android scrollable, where when a
     // scroll action is performed, a new item is added to the list.
     const site = `
@@ -45,19 +46,18 @@
         });
       </script>
       `;
-    this.runWithFakeScrollable(
-        site, {
-          numChildrenBeforeScroll_: -1,
-          beforeScroll: (list) => {
-            this.numChildrenBeforeScroll_ = list.children.length;
-          },
-          scrollFinished: (list) =>
-              list.children.length !== this.numChildrenBeforeScroll_
-        },
-        callback);
+    return await this.runWithFakeScrollable(site, {
+      numChildrenBeforeScroll_: -1,
+      beforeScroll: (list) => {
+        this.numChildrenBeforeScroll_ = list.children.length;
+      },
+      scrollFinished: (list) =>
+          list.children.length !== this.numChildrenBeforeScroll_
+    });
   }
 
-  runWithFakeArcRecyclerView(callback) {
+  /** @return {chrome.automation.AutomationNode} */
+  async runWithFakeArcRecyclerView() {
     // This simulates a scrolling behavior in Android RecyclerView, where when a
     // scroll action is performed, the previously visible items disappear and
     // the new items are added to the list.
@@ -86,17 +86,15 @@
         });
       </script>
       `;
-    this.runWithFakeScrollable(
-        site, {
-          childrenBeforeScroll_: [],
-          beforeScroll: (list) => {
-            this.childrenBeforeScroll_ = list.children;
-          },
-          scrollFinished: (list) => list.children.length === 2 &&
-              list.children[0] !== this.childrenBeforeScroll_[0] &&
-              list.children[1] !== this.childrenBeforeScroll_[1]
-        },
-        callback);
+    return await this.runWithFakeScrollable(site, {
+      childrenBeforeScroll_: [],
+      beforeScroll: (list) => {
+        this.childrenBeforeScroll_ = list.children;
+      },
+      scrollFinished: (list) => list.children.length === 2 &&
+          list.children[0] !== this.childrenBeforeScroll_[0] &&
+          list.children[1] !== this.childrenBeforeScroll_[1]
+    });
   }
 
   /**
@@ -115,213 +113,213 @@
    *   beforeScroll: called before performing a fake scrolling (click action).
    *   scrollFinished: return if the scrolling has finished and the event
    *     listener can be invoked.
-   * @param {Function} callback
+   * @return {chrome.automation.AutomationNode}
    */
-  runWithFakeScrollable(site, scrolledPredicate, callback) {
-    this.runWithLoadedTree(site, function(root) {
-      const list = root.find({role: RoleType.LIST});
-      Object.defineProperty(list, 'focusable', {get: () => false});
-      Object.defineProperty(list, 'scrollable', {get: () => true});
-      Object.defineProperty(list, 'standardActions', {
-        get: () =>
-            [chrome.automation.ActionType.SCROLL_FORWARD,
-             chrome.automation.ActionType.SCROLL_BACKWARD]
-      });
+  async runWithFakeScrollable(site, scrolledPredicate) {
+    const root = await this.runWithLoadedTree(site);
+    const list = root.find({role: RoleType.LIST});
+    Object.defineProperty(list, 'focusable', {get: () => false});
+    Object.defineProperty(list, 'scrollable', {get: () => true});
+    Object.defineProperty(list, 'standardActions', {
+      get: () =>
+          [chrome.automation.ActionType.SCROLL_FORWARD,
+           chrome.automation.ActionType.SCROLL_BACKWARD]
+    });
 
-      // Create a fake addEventListener to dispatch an event listener of
-      // SCROLL_POSITION_CHANGED.
-      let eventListener;
-      const originalAddEventListenerFunc = list.addEventListener.bind(list);
-      list.addEventListener = (eventType, callback, capture) => {
-        if (eventType === EventType.SCROLL_POSITION_CHANGED) {
-          eventListener = callback;
-          return;
-        } else if (
-            eventType === EventType.SCROLL_HORIZONTAL_POSITION_CHANGED ||
-            eventType === EventType.SCROLL_VERTICAL_POSITION_CHANGED) {
-          // Do nothing to prevent catching scroll events dispatched from web.
+    // Create a fake addEventListener to dispatch an event listener of
+    // SCROLL_POSITION_CHANGED.
+    let eventListener;
+    const originalAddEventListenerFunc = list.addEventListener.bind(list);
+    list.addEventListener = (eventType, callback, capture) => {
+      if (eventType === EventType.SCROLL_POSITION_CHANGED) {
+        eventListener = callback;
+        return;
+      } else if (
+          eventType === EventType.SCROLL_HORIZONTAL_POSITION_CHANGED ||
+          eventType === EventType.SCROLL_VERTICAL_POSITION_CHANGED) {
+        // Do nothing to prevent catching scroll events dispatched from web.
+        return;
+      }
+      originalAddEventListenerFunc(eventType, callback, capture);
+    };
+
+    // Create a fake scrollForward and scrollBackward actions.
+    const fakeScrollFunc = (cb) => {
+      scrolledPredicate.beforeScroll(list);
+      const listener = (ev) => {
+        if (!scrolledPredicate.scrollFinished(list)) {
           return;
         }
-        originalAddEventListenerFunc(eventType, callback, capture);
+        list.removeEventListener(EventType.CHILDREN_CHANGED, listener, true);
+        eventListener();
       };
+      list.addEventListener(EventType.CHILDREN_CHANGED, listener, true);
 
-      // Create a fake scrollForward and scrollBackward actions.
-      const fakeScrollFunc = (cb) => {
-        scrolledPredicate.beforeScroll(list);
-        const listener = (ev) => {
-          if (!scrolledPredicate.scrollFinished(list)) {
-            return;
-          }
-          list.removeEventListener(EventType.CHILDREN_CHANGED, listener, true);
-          eventListener();
-        };
-        list.addEventListener(EventType.CHILDREN_CHANGED, listener, true);
+      // Invoke 'click' event on the list, which updates the list items.
+      list.doDefault();
+      cb(true);
+    };
+    list.scrollForward = fakeScrollFunc;
+    list.scrollBackward = fakeScrollFunc;
 
-        // Invoke 'click' event on the list, which updates the list items.
-        list.doDefault();
-        cb(true);
-      };
-      list.scrollForward = fakeScrollFunc;
-      list.scrollBackward = fakeScrollFunc;
-
-      callback(root);
-    });
+    return root;
   }
 };
 
 TEST_F(
-    'ChromeVoxAutoScrollHandlerTest', 'DontScrollInSameScrollable', function() {
-      this.runWithFakeArcSimpleScrollable(function(root) {
-        const handler = new AutoScrollHandler();
+    'ChromeVoxAutoScrollHandlerTest', 'DontScrollInSameScrollable',
+    async function() {
+      const root = await this.runWithFakeArcSimpleScrollable();
+      const handler = new AutoScrollHandler();
 
-        const list = root.find({role: RoleType.LIST});
-        const firstItemCursor = cursors.Range.fromNode(list.firstChild);
-        const lastItemCursor = cursors.Range.fromNode(list.lastChild);
+      const list = root.find({role: RoleType.LIST});
+      const firstItemCursor = cursors.Range.fromNode(list.firstChild);
+      const lastItemCursor = cursors.Range.fromNode(list.lastChild);
 
-        ChromeVoxState.instance.navigateToRange(firstItemCursor);
+      ChromeVoxState.instance.navigateToRange(firstItemCursor);
 
-        assertTrue(handler.onCommandNavigation(
-            lastItemCursor, constants.Dir.FORWARD, /*pred=*/ null,
-            /*speechProps=*/ null));
-      });
+      assertTrue(handler.onCommandNavigation(
+          lastItemCursor, constants.Dir.FORWARD, /*pred=*/ null,
+          /*speechProps=*/ null));
     });
 
 TEST_F(
-    'ChromeVoxAutoScrollHandlerTest', 'PreventMultipleScrolling', function() {
-      this.runWithFakeArcSimpleScrollable(function(root) {
-        const handler = new AutoScrollHandler();
+    'ChromeVoxAutoScrollHandlerTest', 'PreventMultipleScrolling',
+    async function() {
+      const root = await this.runWithFakeArcSimpleScrollable();
+      const handler = new AutoScrollHandler();
 
-        const list = root.find({role: RoleType.LIST});
-        const rootCursor = cursors.Range.fromNode(root);
-        const firstItemCursor = cursors.Range.fromNode(list.firstChild);
-        const lastItemCursor = cursors.Range.fromNode(list.lastChild);
+      const list = root.find({role: RoleType.LIST});
+      const rootCursor = cursors.Range.fromNode(root);
+      const firstItemCursor = cursors.Range.fromNode(list.firstChild);
+      const lastItemCursor = cursors.Range.fromNode(list.lastChild);
 
-        ChromeVoxState.instance.navigateToRange(lastItemCursor);
+      ChromeVoxState.instance.navigateToRange(lastItemCursor);
 
-        // Make scrolling action void, so that the second invocation should be
-        // ignored.
-        list.scrollForward = () => {};
+      // Make scrolling action void, so that the second invocation should be
+      // ignored.
+      list.scrollForward = () => {};
 
-        assertFalse(handler.onCommandNavigation(
-            rootCursor, constants.Dir.FORWARD, /*pred=*/ null,
-            /*speechProps=*/ null));
+      assertFalse(handler.onCommandNavigation(
+          rootCursor, constants.Dir.FORWARD, /*pred=*/ null,
+          /*speechProps=*/ null));
 
-        assertFalse(handler.onCommandNavigation(
-            firstItemCursor, constants.Dir.FORWARD, /*pred=*/ null,
-            /*speechProps=*/ null));
-      });
+      assertFalse(handler.onCommandNavigation(
+          firstItemCursor, constants.Dir.FORWARD, /*pred=*/ null,
+          /*speechProps=*/ null));
     });
 
-TEST_F('ChromeVoxAutoScrollHandlerTest', 'ScrollForward', function() {
+TEST_F('ChromeVoxAutoScrollHandlerTest', 'ScrollForward', async function() {
   const mockFeedback = this.createMockFeedback();
-  this.runWithFakeArcSimpleScrollable(function(root) {
-    mockFeedback.expectSpeech('1st item')
-        .call(doCmd('nextObject'))
-        .expectSpeech('2nd item')
-        .call(doCmd('nextObject'))
-        .expectSpeech('3rd item')
-        .call(doCmd('nextObject'))
-        .expectSpeech('4th item')
-        .call(doCmd('nextObject'))
-        .expectSpeech('5th item')
-        .replay();
-  });
+  const root = await this.runWithFakeArcSimpleScrollable();
+  mockFeedback.expectSpeech('1st item')
+      .call(doCmd('nextObject'))
+      .expectSpeech('2nd item')
+      .call(doCmd('nextObject'))
+      .expectSpeech('3rd item')
+      .call(doCmd('nextObject'))
+      .expectSpeech('4th item')
+      .call(doCmd('nextObject'))
+      .expectSpeech('5th item')
+      .replay();
 });
 
 TEST_F(
-    'ChromeVoxAutoScrollHandlerTest', 'ScrollForwardReturnsFalse', function() {
+    'ChromeVoxAutoScrollHandlerTest', 'ScrollForwardReturnsFalse',
+    async function() {
       const mockFeedback = this.createMockFeedback();
-      this.runWithFakeArcSimpleScrollable(function(root) {
-        const list = root.find({role: RoleType.LIST});
-        list.scrollForward = (callback) => callback(false);
+      const root = await this.runWithFakeArcSimpleScrollable();
+      const list = root.find({role: RoleType.LIST});
+      list.scrollForward = (callback) => callback(false);
 
-        mockFeedback.expectSpeech('1st item')
-            .call(doCmd('nextObject'))
-            .expectSpeech('2nd item')
-            .call(doCmd('nextObject'))
-            .expectSpeech('3rd item')
-            .call(doCmd('nextObject'))
-            .expectSpeech('hello')
-            .call(doCmd('nextObject'))
-            .expectSpeech('world')
-            .replay();
-      });
+      mockFeedback.expectSpeech('1st item')
+          .call(doCmd('nextObject'))
+          .expectSpeech('2nd item')
+          .call(doCmd('nextObject'))
+          .expectSpeech('3rd item')
+          .call(doCmd('nextObject'))
+          .expectSpeech('hello')
+          .call(doCmd('nextObject'))
+          .expectSpeech('world')
+          .replay();
     });
 
-TEST_F('ChromeVoxAutoScrollHandlerTest', 'RecyclerViewByObject', function() {
-  const mockFeedback = this.createMockFeedback();
-  this.runWithFakeArcRecyclerView(function(root) {
-    mockFeedback.expectSpeech('1st item')
-        .call(doCmd('nextObject'))
-        .expectSpeech('2nd item')
-        .call(doCmd('nextObject'))  // scroll forward
-        .expectSpeech('3rd item')
-        .call(doCmd('previousObject'))  // scroll backward
-        .expectSpeech('2nd item')
-        .replay();
-  });
-});
+TEST_F(
+    'ChromeVoxAutoScrollHandlerTest', 'RecyclerViewByObject', async function() {
+      const mockFeedback = this.createMockFeedback();
+      const root = await this.runWithFakeArcRecyclerView();
+      mockFeedback.expectSpeech('1st item')
+          .call(doCmd('nextObject'))
+          .expectSpeech('2nd item')
+          .call(doCmd('nextObject'))  // scroll forward
+          .expectSpeech('3rd item')
+          .call(doCmd('previousObject'))  // scroll backward
+          .expectSpeech('2nd item')
+          .replay();
+    });
 
-TEST_F('ChromeVoxAutoScrollHandlerTest', 'RecyclerViewByWord', function() {
-  const mockFeedback = this.createMockFeedback();
-  this.runWithFakeArcRecyclerView(function(root) {
-    mockFeedback.expectSpeech('1st item')
-        .call(doCmd('nextObject'))
-        .expectSpeech('2nd item')
-        .call(doCmd('nextWord'))
-        .expectSpeech('item')
-        .call(doCmd('nextWord'))  // scroll forward
-        .expectSpeech('3rd')
-        .call(doCmd('previousWord'))  // scroll backward
-        .expectSpeech('item')
-        .call(doCmd('previousWord'))
-        .expectSpeech('2nd')
-        .replay();
-  });
-});
+TEST_F(
+    'ChromeVoxAutoScrollHandlerTest', 'RecyclerViewByWord', async function() {
+      const mockFeedback = this.createMockFeedback();
+      const root = await this.runWithFakeArcRecyclerView();
+      mockFeedback.expectSpeech('1st item')
+          .call(doCmd('nextObject'))
+          .expectSpeech('2nd item')
+          .call(doCmd('nextWord'))
+          .expectSpeech('item')
+          .call(doCmd('nextWord'))  // scroll forward
+          .expectSpeech('3rd')
+          .call(doCmd('previousWord'))  // scroll backward
+          .expectSpeech('item')
+          .call(doCmd('previousWord'))
+          .expectSpeech('2nd')
+          .replay();
+    });
 
-TEST_F('ChromeVoxAutoScrollHandlerTest', 'RecyclerViewByCharacter', function() {
-  const mockFeedback = this.createMockFeedback();
-  this.runWithFakeArcRecyclerView(function(root) {
-    mockFeedback.expectSpeech('1st item')
-        .call(doCmd('nextObject'))
-        .expectSpeech('2nd item')
-        .call(doCmd('nextWord'))
-        .expectSpeech('item')
-        .call(doCmd('nextCharacter'))
-        .expectSpeech('t')
-        .call(doCmd('nextCharacter'))
-        .expectSpeech('e')
-        .call(doCmd('nextCharacter'))
-        .expectSpeech('m')
-        .call(doCmd('nextCharacter'))  // scroll forward
-        .expectSpeech('3')
-        .call(doCmd('nextCharacter'))
-        .expectSpeech('r')
-        .call(doCmd('previousCharacter'))
-        .expectSpeech('3')
-        .call(doCmd('previousCharacter'))  // scroll backward
-        .expectSpeech('m')
-        .call(doCmd('previousCharacter'))
-        .expectSpeech('e')
-        .replay();
-  });
-});
+TEST_F(
+    'ChromeVoxAutoScrollHandlerTest', 'RecyclerViewByCharacter',
+    async function() {
+      const mockFeedback = this.createMockFeedback();
+      const root = await this.runWithFakeArcRecyclerView();
+      mockFeedback.expectSpeech('1st item')
+          .call(doCmd('nextObject'))
+          .expectSpeech('2nd item')
+          .call(doCmd('nextWord'))
+          .expectSpeech('item')
+          .call(doCmd('nextCharacter'))
+          .expectSpeech('t')
+          .call(doCmd('nextCharacter'))
+          .expectSpeech('e')
+          .call(doCmd('nextCharacter'))
+          .expectSpeech('m')
+          .call(doCmd('nextCharacter'))  // scroll forward
+          .expectSpeech('3')
+          .call(doCmd('nextCharacter'))
+          .expectSpeech('r')
+          .call(doCmd('previousCharacter'))
+          .expectSpeech('3')
+          .call(doCmd('previousCharacter'))  // scroll backward
+          .expectSpeech('m')
+          .call(doCmd('previousCharacter'))
+          .expectSpeech('e')
+          .replay();
+    });
 
-TEST_F('ChromeVoxAutoScrollHandlerTest', 'RecyclerViewByPredicate', function() {
-  // TODO(hirokisato): This test fails without '<p>unrelated content</p>' in the
-  // tree, because the next item of '2nd item' without scrolling is '1st item',
-  // and the scrollable is LCA, so auto scrolling is not invoked. We should fix
-  // this corner case.
-  const mockFeedback = this.createMockFeedback();
-  this.runWithFakeArcRecyclerView(function(root) {
-    mockFeedback.expectSpeech('1st item')
-        .call(doCmd('nextSimilarItem'))
-        .expectSpeech('2nd item')
-        .call(doCmd('nextSimilarItem'))  // scroll forward
-        .expectSpeech('3rd item')
-        .call(doCmd('previousSimilarItem'))  // scroll backward
-        .expectSpeech('2nd item')
-        .replay();
-  });
-});
+TEST_F(
+    'ChromeVoxAutoScrollHandlerTest', 'RecyclerViewByPredicate',
+    async function() {
+      // TODO(hirokisato): This test fails without '<p>unrelated content</p>' in
+      // the tree, because the next item of '2nd item' without scrolling is '1st
+      // item', and the scrollable is LCA, so auto scrolling is not invoked. We
+      // should fix this corner case.
+      const mockFeedback = this.createMockFeedback();
+      await this.runWithFakeArcRecyclerView();
+      mockFeedback.expectSpeech('1st item')
+          .call(doCmd('nextSimilarItem'))
+          .expectSpeech('2nd item')
+          .call(doCmd('nextSimilarItem'))  // scroll forward
+          .expectSpeech('3rd item')
+          .call(doCmd('previousSimilarItem'))  // scroll backward
+          .expectSpeech('2nd item')
+          .replay();
+    });
diff --git a/chrome/browser/resources/chromeos/accessibility/chromevox/background/background_test.js b/chrome/browser/resources/chromeos/accessibility/chromevox/background/background_test.js
index 42c7041..728100a 100644
--- a/chrome/browser/resources/chromeos/accessibility/chromevox/background/background_test.js
+++ b/chrome/browser/resources/chromeos/accessibility/chromevox/background/background_test.js
@@ -39,6 +39,9 @@
         'GestureCommandHandler',
         '/chromevox/background/gesture_command_handler.js');
     await importModule(
+        'PageLoadSoundHandler',
+        '/chromevox/background/page_load_sound_handler.js');
+    await importModule(
         'PointerHandler', '/chromevox/background/pointer_handler.js');
     await super.setUpDeferred();
   }
@@ -220,184 +223,177 @@
 });
 
 /** Tests consistency of navigating forward and backward. */
-TEST_F('ChromeVoxBackgroundTest', 'ForwardBackwardNavigation', function() {
-  const mockFeedback = this.createMockFeedback();
-  this.runWithLoadedTree(this.linksAndHeadingsDoc, function() {
-    mockFeedback.expectSpeech('start').expectBraille('start');
+TEST_F(
+    'ChromeVoxBackgroundTest', 'ForwardBackwardNavigation', async function() {
+      const mockFeedback = this.createMockFeedback();
+      await this.runWithLoadedTree(this.linksAndHeadingsDoc);
+      mockFeedback.expectSpeech('start').expectBraille('start');
 
-    mockFeedback.call(doCmd('nextLink'))
-        .expectSpeech('alpha', 'Link')
-        .expectBraille('alpha lnk');
-    mockFeedback.call(doCmd('nextLink'))
-        .expectSpeech('beta', 'Link')
-        .expectBraille('beta lnk');
-    mockFeedback.call(doCmd('nextLink'))
-        .expectSpeech('delta', 'Link')
-        .expectBraille('delta lnk');
-    mockFeedback.call(doCmd('previousLink'))
-        .expectSpeech('beta', 'Link')
-        .expectBraille('beta lnk');
-    mockFeedback.call(doCmd('nextHeading'))
-        .expectSpeech('charlie', 'Heading 1')
-        .expectBraille('charlie h1');
-    mockFeedback.call(doCmd('nextHeading'))
-        .expectSpeech('foxtraut', 'Heading 2')
-        .expectBraille('foxtraut h2');
-    mockFeedback.call(doCmd('previousHeading'))
-        .expectSpeech('charlie', 'Heading 1')
-        .expectBraille('charlie h1');
+      mockFeedback.call(doCmd('nextLink'))
+          .expectSpeech('alpha', 'Link')
+          .expectBraille('alpha lnk');
+      mockFeedback.call(doCmd('nextLink'))
+          .expectSpeech('beta', 'Link')
+          .expectBraille('beta lnk');
+      mockFeedback.call(doCmd('nextLink'))
+          .expectSpeech('delta', 'Link')
+          .expectBraille('delta lnk');
+      mockFeedback.call(doCmd('previousLink'))
+          .expectSpeech('beta', 'Link')
+          .expectBraille('beta lnk');
+      mockFeedback.call(doCmd('nextHeading'))
+          .expectSpeech('charlie', 'Heading 1')
+          .expectBraille('charlie h1');
+      mockFeedback.call(doCmd('nextHeading'))
+          .expectSpeech('foxtraut', 'Heading 2')
+          .expectBraille('foxtraut h2');
+      mockFeedback.call(doCmd('previousHeading'))
+          .expectSpeech('charlie', 'Heading 1')
+          .expectBraille('charlie h1');
 
-    mockFeedback.call(doCmd('nextObject'))
-        .expectSpeech('delta', 'Link')
-        .expectBraille('delta lnk');
-    mockFeedback.call(doCmd('nextObject'))
-        .expectSpeech('echo', 'Link')
-        .expectBraille('echo lnk');
-    mockFeedback.call(doCmd('nextObject'))
-        .expectSpeech('foxtraut', 'Heading 2')
-        .expectBraille('foxtraut h2');
-    mockFeedback.call(doCmd('nextObject'))
-        .expectSpeech('end')
-        .expectBraille('end');
-    mockFeedback.call(doCmd('previousObject'))
-        .expectSpeech('foxtraut', 'Heading 2')
-        .expectBraille('foxtraut h2');
-    mockFeedback.call(doCmd('nextLine')).expectSpeech('foxtraut');
-    mockFeedback.call(doCmd('nextLine'))
-        .expectSpeech('end', 'of test')
-        .expectBraille('endof test');
+      mockFeedback.call(doCmd('nextObject'))
+          .expectSpeech('delta', 'Link')
+          .expectBraille('delta lnk');
+      mockFeedback.call(doCmd('nextObject'))
+          .expectSpeech('echo', 'Link')
+          .expectBraille('echo lnk');
+      mockFeedback.call(doCmd('nextObject'))
+          .expectSpeech('foxtraut', 'Heading 2')
+          .expectBraille('foxtraut h2');
+      mockFeedback.call(doCmd('nextObject'))
+          .expectSpeech('end')
+          .expectBraille('end');
+      mockFeedback.call(doCmd('previousObject'))
+          .expectSpeech('foxtraut', 'Heading 2')
+          .expectBraille('foxtraut h2');
+      mockFeedback.call(doCmd('nextLine')).expectSpeech('foxtraut');
+      mockFeedback.call(doCmd('nextLine'))
+          .expectSpeech('end', 'of test')
+          .expectBraille('endof test');
 
-    mockFeedback.call(doCmd('jumpToTop'))
-        .expectSpeech('start')
-        .expectBraille('start');
-    mockFeedback.call(doCmd('jumpToBottom'))
-        .expectSpeech('of test')
-        .expectBraille('of test');
+      mockFeedback.call(doCmd('jumpToTop'))
+          .expectSpeech('start')
+          .expectBraille('start');
+      mockFeedback.call(doCmd('jumpToBottom'))
+          .expectSpeech('of test')
+          .expectBraille('of test');
 
-    mockFeedback.replay();
-  });
-});
+      mockFeedback.replay();
+    });
 
-TEST_F('ChromeVoxBackgroundTest', 'CaretNavigation', function() {
+TEST_F('ChromeVoxBackgroundTest', 'CaretNavigation', async function() {
   // TODO(plundblad): Add braille expectations when crbug.com/523285 is fixed.
   const mockFeedback = this.createMockFeedback();
-  this.runWithLoadedTree(this.linksAndHeadingsDoc, function() {
-    mockFeedback.expectSpeech('start');
-    mockFeedback.call(doCmd('nextCharacter')).expectSpeech('t');
-    mockFeedback.call(doCmd('nextCharacter')).expectSpeech('a');
-    mockFeedback.call(doCmd('nextWord')).expectSpeech('alpha', 'Link');
-    mockFeedback.call(doCmd('nextWord')).expectSpeech('beta', 'Link');
-    mockFeedback.call(doCmd('previousWord')).expectSpeech('alpha', 'Link');
-    mockFeedback.call(doCmd('nextWord')).expectSpeech('beta', 'Link');
-    mockFeedback.call(doCmd('nextWord')).expectSpeech('charlie', 'Heading 1');
-    mockFeedback.call(doCmd('nextLine')).expectSpeech('delta', 'Link');
-    mockFeedback.call(doCmd('nextLine')).expectSpeech('echo', 'Link');
-    mockFeedback.call(doCmd('nextLine')).expectSpeech('foxtraut', 'Heading 2');
-    mockFeedback.call(doCmd('nextLine')).expectSpeech('end', 'of test');
-    mockFeedback.call(doCmd('nextCharacter')).expectSpeech('n');
-    mockFeedback.call(doCmd('previousCharacter')).expectSpeech('e');
-    mockFeedback.call(doCmd('previousCharacter'))
-        .expectSpeech('t', 'Heading 2');
-    mockFeedback.call(doCmd('previousWord')).expectSpeech('foxtraut');
-    mockFeedback.call(doCmd('previousWord')).expectSpeech('echo', 'Link');
-    mockFeedback.call(doCmd('previousCharacter')).expectSpeech('a', 'Link');
-    mockFeedback.call(doCmd('previousCharacter')).expectSpeech('t');
-    mockFeedback.call(doCmd('nextWord')).expectSpeech('echo', 'Link');
-    mockFeedback.replay();
-  });
+  await this.runWithLoadedTree(this.linksAndHeadingsDoc);
+  mockFeedback.expectSpeech('start');
+  mockFeedback.call(doCmd('nextCharacter')).expectSpeech('t');
+  mockFeedback.call(doCmd('nextCharacter')).expectSpeech('a');
+  mockFeedback.call(doCmd('nextWord')).expectSpeech('alpha', 'Link');
+  mockFeedback.call(doCmd('nextWord')).expectSpeech('beta', 'Link');
+  mockFeedback.call(doCmd('previousWord')).expectSpeech('alpha', 'Link');
+  mockFeedback.call(doCmd('nextWord')).expectSpeech('beta', 'Link');
+  mockFeedback.call(doCmd('nextWord')).expectSpeech('charlie', 'Heading 1');
+  mockFeedback.call(doCmd('nextLine')).expectSpeech('delta', 'Link');
+  mockFeedback.call(doCmd('nextLine')).expectSpeech('echo', 'Link');
+  mockFeedback.call(doCmd('nextLine')).expectSpeech('foxtraut', 'Heading 2');
+  mockFeedback.call(doCmd('nextLine')).expectSpeech('end', 'of test');
+  mockFeedback.call(doCmd('nextCharacter')).expectSpeech('n');
+  mockFeedback.call(doCmd('previousCharacter')).expectSpeech('e');
+  mockFeedback.call(doCmd('previousCharacter')).expectSpeech('t', 'Heading 2');
+  mockFeedback.call(doCmd('previousWord')).expectSpeech('foxtraut');
+  mockFeedback.call(doCmd('previousWord')).expectSpeech('echo', 'Link');
+  mockFeedback.call(doCmd('previousCharacter')).expectSpeech('a', 'Link');
+  mockFeedback.call(doCmd('previousCharacter')).expectSpeech('t');
+  mockFeedback.call(doCmd('nextWord')).expectSpeech('echo', 'Link');
+  mockFeedback.replay();
 });
 
 /** Tests that individual buttons are stops for move-by-word functionality. */
 TEST_F(
     'ChromeVoxBackgroundTest', 'CaretNavigationMoveThroughButtonByWord',
-    function() {
+    async function() {
       const mockFeedback = this.createMockFeedback();
-      this.runWithLoadedTree(this.buttonDoc, function() {
-        mockFeedback.expectSpeech('start');
-        mockFeedback.call(doCmd('nextObject'))
-            .expectSpeech('hello button one', 'Button');
-        mockFeedback.call(doCmd('previousWord')).expectSpeech('start');
-        mockFeedback.call(doCmd('nextWord')).expectSpeech('hello');
-        mockFeedback.call(doCmd('nextWord')).expectSpeech('button');
-        mockFeedback.call(doCmd('nextWord')).expectSpeech('one');
-        mockFeedback.call(doCmd('nextWord')).expectSpeech('cats');
-        mockFeedback.call(doCmd('nextWord')).expectSpeech('hello');
-        mockFeedback.call(doCmd('nextWord')).expectSpeech('button');
-        mockFeedback.call(doCmd('nextWord')).expectSpeech('two');
-        mockFeedback.call(doCmd('nextWord')).expectSpeech('end');
-        mockFeedback.call(doCmd('previousWord')).expectSpeech('two');
-        mockFeedback.call(doCmd('previousWord')).expectSpeech('button');
-        mockFeedback.call(doCmd('previousWord')).expectSpeech('hello');
-        mockFeedback.call(doCmd('previousWord')).expectSpeech('cats');
-        mockFeedback.call(doCmd('previousWord')).expectSpeech('one');
-        mockFeedback.call(doCmd('previousWord')).expectSpeech('button');
-        mockFeedback.call(doCmd('previousWord')).expectSpeech('hello');
-        mockFeedback.replay();
-      });
+      await this.runWithLoadedTree(this.buttonDoc);
+      mockFeedback.expectSpeech('start');
+      mockFeedback.call(doCmd('nextObject'))
+          .expectSpeech('hello button one', 'Button');
+      mockFeedback.call(doCmd('previousWord')).expectSpeech('start');
+      mockFeedback.call(doCmd('nextWord')).expectSpeech('hello');
+      mockFeedback.call(doCmd('nextWord')).expectSpeech('button');
+      mockFeedback.call(doCmd('nextWord')).expectSpeech('one');
+      mockFeedback.call(doCmd('nextWord')).expectSpeech('cats');
+      mockFeedback.call(doCmd('nextWord')).expectSpeech('hello');
+      mockFeedback.call(doCmd('nextWord')).expectSpeech('button');
+      mockFeedback.call(doCmd('nextWord')).expectSpeech('two');
+      mockFeedback.call(doCmd('nextWord')).expectSpeech('end');
+      mockFeedback.call(doCmd('previousWord')).expectSpeech('two');
+      mockFeedback.call(doCmd('previousWord')).expectSpeech('button');
+      mockFeedback.call(doCmd('previousWord')).expectSpeech('hello');
+      mockFeedback.call(doCmd('previousWord')).expectSpeech('cats');
+      mockFeedback.call(doCmd('previousWord')).expectSpeech('one');
+      mockFeedback.call(doCmd('previousWord')).expectSpeech('button');
+      mockFeedback.call(doCmd('previousWord')).expectSpeech('hello');
+      mockFeedback.replay();
     });
 
-TEST_F('ChromeVoxBackgroundTest', 'SelectSingleBasic', function() {
+TEST_F('ChromeVoxBackgroundTest', 'SelectSingleBasic', async function() {
   const mockFeedback = this.createMockFeedback();
-  this.runWithLoadedTree(this.formsDoc, function() {
-    mockFeedback.expectSpeech('apple', 'has pop up', 'Collapsed')
-        .expectBraille('apple btn +popup +3 +')
-        .call(press(KeyCode.DOWN))
-        .expectSpeech('grape', /2 of 3/)
-        .expectBraille('grape 2/3')
-        .call(press(KeyCode.DOWN))
-        .expectSpeech('banana', /3 of 3/)
-        .expectBraille('banana 3/3');
-    mockFeedback.replay();
-  });
+  await this.runWithLoadedTree(this.formsDoc);
+  mockFeedback.expectSpeech('apple', 'has pop up', 'Collapsed')
+      .expectBraille('apple btn +popup +3 +')
+      .call(press(KeyCode.DOWN))
+      .expectSpeech('grape', /2 of 3/)
+      .expectBraille('grape 2/3')
+      .call(press(KeyCode.DOWN))
+      .expectSpeech('banana', /3 of 3/)
+      .expectBraille('banana 3/3');
+  mockFeedback.replay();
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'ContinuousRead', function() {
+TEST_F('ChromeVoxBackgroundTest', 'ContinuousRead', async function() {
   const mockFeedback = this.createMockFeedback();
-  this.runWithLoadedTree(this.linksAndHeadingsDoc, function() {
-    mockFeedback.expectSpeech('start')
-        .call(doCmd('readFromHere'))
-        .expectSpeech(
-            'start', 'alpha', 'Link', 'beta', 'Link', 'charlie', 'Heading 1');
-    mockFeedback.replay();
-  });
+  await this.runWithLoadedTree(this.linksAndHeadingsDoc);
+  mockFeedback.expectSpeech('start')
+      .call(doCmd('readFromHere'))
+      .expectSpeech(
+          'start', 'alpha', 'Link', 'beta', 'Link', 'charlie', 'Heading 1');
+  mockFeedback.replay();
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'InitialFocus', function() {
+TEST_F('ChromeVoxBackgroundTest', 'InitialFocus', async function() {
   const mockFeedback = this.createMockFeedback();
-  this.runWithLoadedTree('<a href="a">a</a>', function(rootNode) {
-    mockFeedback.expectSpeech('a').expectSpeech('Link');
-    mockFeedback.replay();
-  });
+  await this.runWithLoadedTree('<a href="a">a</a>');
+  mockFeedback.expectSpeech('a').expectSpeech('Link');
+  mockFeedback.replay();
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'AriaLabel', function() {
+TEST_F('ChromeVoxBackgroundTest', 'AriaLabel', async function() {
   const mockFeedback = this.createMockFeedback();
   const site = '<a aria-label="foo" href="a">a</a>';
-  this.runWithLoadedTree(site, function(rootNode) {
-    rootNode.find({role: RoleType.LINK}).focus();
-    mockFeedback.expectSpeech('foo')
-        .expectSpeech('Link')
-        .expectSpeech('Press Search+Space to activate')
-        .expectBraille('foo lnk');
-    mockFeedback.replay();
-  });
+  const rootNode = await this.runWithLoadedTree(site);
+  rootNode.find({role: RoleType.LINK}).focus();
+  mockFeedback.expectSpeech('foo')
+      .expectSpeech('Link')
+      .expectSpeech('Press Search+Space to activate')
+      .expectBraille('foo lnk');
+  mockFeedback.replay();
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'ShowContextMenu', function() {
+TEST_F('ChromeVoxBackgroundTest', 'ShowContextMenu', async function() {
   const mockFeedback = this.createMockFeedback();
-  this.runWithLoadedTree('<p>before</p><a href="a">a</a>', function(rootNode) {
-    const go = rootNode.find({role: RoleType.LINK});
-    mockFeedback.call(go.focus.bind(go))
-        .expectSpeech('a', 'Link')
-        .call(doCmd('contextMenu'))
-        .expectSpeech(/menu opened/)
-        .call(press(KeyCode.ESCAPE))
-        .expectSpeech('a', 'Link');
-    mockFeedback.replay();
-  }.bind(this));
+  const rootNode =
+      await this.runWithLoadedTree('<p>before</p><a href="a">a</a>');
+  const go = rootNode.find({role: RoleType.LINK});
+  mockFeedback.call(go.focus.bind(go))
+      .expectSpeech('a', 'Link')
+      .call(doCmd('contextMenu'))
+      .expectSpeech(/menu opened/)
+      .call(press(KeyCode.ESCAPE))
+      .expectSpeech('a', 'Link');
+  mockFeedback.replay();
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'BrailleRouting', function() {
+TEST_F('ChromeVoxBackgroundTest', 'BrailleRouting', async function() {
   const mockFeedback = this.createMockFeedback();
   const route = function(position) {
     assertTrue(ChromeVoxState.instance.onBrailleKeyEvent(
@@ -417,74 +413,71 @@
       }, false);
     </script>
   `;
-  this.runWithLoadedTree(site, function(rootNode) {
-    const button1 =
-        rootNode.find({role: RoleType.BUTTON, attributes: {name: 'Click me'}});
-    const textField = rootNode.find({role: RoleType.TEXT_FIELD});
-    mockFeedback.expectBraille('start')
-        .call(button1.focus.bind(button1))
-        .expectBraille(/^Click me btn/)
-        .call(route.bind(null, 5))
-        .expectBraille(/Focus me btn/)
-        .call(textField.focus.bind(textField))
-        .expectBraille('Edit me ed', {startIndex: 0})
-        .call(route.bind(null, 3))
-        .expectBraille('Edit me ed', {startIndex: 3})
-        .call(function() {
-          assertEquals(3, textField.textSelStart);
-        });
-    mockFeedback.replay();
-  });
+  const rootNode = await this.runWithLoadedTree(site);
+  const button1 =
+      rootNode.find({role: RoleType.BUTTON, attributes: {name: 'Click me'}});
+  const textField = rootNode.find({role: RoleType.TEXT_FIELD});
+  mockFeedback.expectBraille('start')
+      .call(button1.focus.bind(button1))
+      .expectBraille(/^Click me btn/)
+      .call(route.bind(null, 5))
+      .expectBraille(/Focus me btn/)
+      .call(textField.focus.bind(textField))
+      .expectBraille('Edit me ed', {startIndex: 0})
+      .call(route.bind(null, 3))
+      .expectBraille('Edit me ed', {startIndex: 3})
+      .call(function() {
+        assertEquals(3, textField.textSelStart);
+      });
+  mockFeedback.replay();
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'FocusInputElement', function() {
+TEST_F('ChromeVoxBackgroundTest', 'FocusInputElement', async function() {
   const mockFeedback = this.createMockFeedback();
   const site = `
       <input id="name" value="Lancelot">
       <input id="quest" value="Grail">
       <input id="color" value="Blue">
     `;
-  this.runWithLoadedTree(site, function(rootNode) {
-    const name = rootNode.find({attributes: {value: 'Lancelot'}});
-    const quest = rootNode.find({attributes: {value: 'Grail'}});
-    const color = rootNode.find({attributes: {value: 'Blue'}});
+  const rootNode = await this.runWithLoadedTree(site);
+  const name = rootNode.find({attributes: {value: 'Lancelot'}});
+  const quest = rootNode.find({attributes: {value: 'Grail'}});
+  const color = rootNode.find({attributes: {value: 'Blue'}});
 
-    mockFeedback.call(quest.focus.bind(quest))
-        .expectSpeech('Grail', 'Edit text')
-        .call(color.focus.bind(color))
-        .expectSpeech('Blue', 'Edit text')
-        .call(name.focus.bind(name))
-        .expectNextSpeechUtteranceIsNot('Blue')
-        .expectSpeech('Lancelot', 'Edit text');
-    mockFeedback.replay();
-  }.bind(this));
+  mockFeedback.call(quest.focus.bind(quest))
+      .expectSpeech('Grail', 'Edit text')
+      .call(color.focus.bind(color))
+      .expectSpeech('Blue', 'Edit text')
+      .call(name.focus.bind(name))
+      .expectNextSpeechUtteranceIsNot('Blue')
+      .expectSpeech('Lancelot', 'Edit text');
+  mockFeedback.replay();
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'UseEditableState', function() {
+TEST_F('ChromeVoxBackgroundTest', 'UseEditableState', async function() {
   const site = `
       <input type="text"></input>
       <p tabindex=0>hi</p>
     `;
-  this.runWithLoadedTree(site, async function(rootNode) {
-    const nonEditable = rootNode.find({role: RoleType.PARAGRAPH});
-    const editable = rootNode.find({role: RoleType.TEXT_FIELD});
+  const rootNode = await this.runWithLoadedTree(site);
+  const nonEditable = rootNode.find({role: RoleType.PARAGRAPH});
+  const editable = rootNode.find({role: RoleType.TEXT_FIELD});
 
-    nonEditable.focus();
-    await new Promise(resolve => {
-      this.listenOnce(nonEditable, 'focus', resolve);
-    });
-    assertTrue(!DesktopAutomationInterface.instance.textEditHandler);
-
-    editable.focus();
-    await new Promise(resolve => {
-      this.listenOnce(editable, 'focus', resolve);
-    });
-    assertNotNullNorUndefined(
-        DesktopAutomationInterface.instance.textEditHandler);
+  nonEditable.focus();
+  await new Promise(resolve => {
+    this.listenOnce(nonEditable, 'focus', resolve);
   });
+  assertTrue(!DesktopAutomationInterface.instance.textEditHandler);
+
+  editable.focus();
+  await new Promise(resolve => {
+    this.listenOnce(editable, 'focus', resolve);
+  });
+  assertNotNullNorUndefined(
+      DesktopAutomationInterface.instance.textEditHandler);
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'EarconsForControls', function() {
+TEST_F('ChromeVoxBackgroundTest', 'EarconsForControls', async function() {
   const mockFeedback = this.createMockFeedback();
   const site = `
       <p>Initial focus will be on something that's not a control.</p>
@@ -497,38 +490,37 @@
       <select><option>2</option></select>
       <input type=range value=5>
     `;
-  this.runWithLoadedTree(site, function(rootNode) {
-    mockFeedback.call(doCmd('nextObject'))
-        .expectSpeech('MyLink')
-        .expectEarcon(Earcon.LINK)
-        .call(doCmd('nextObject'))
-        .expectSpeech('MyButton')
-        .expectEarcon(Earcon.BUTTON)
-        .call(doCmd('nextObject'))
-        .expectSpeech('Check box')
-        .expectEarcon(Earcon.CHECK_OFF)
-        .call(doCmd('nextObject'))
-        .expectSpeech('Check box')
-        .expectEarcon(Earcon.CHECK_ON)
-        .call(doCmd('nextObject'))
-        .expectSpeech('Edit text')
-        .expectEarcon(Earcon.EDITABLE_TEXT)
+  const rootNode = await this.runWithLoadedTree(site);
+  mockFeedback.call(doCmd('nextObject'))
+      .expectSpeech('MyLink')
+      .expectEarcon(Earcon.LINK)
+      .call(doCmd('nextObject'))
+      .expectSpeech('MyButton')
+      .expectEarcon(Earcon.BUTTON)
+      .call(doCmd('nextObject'))
+      .expectSpeech('Check box')
+      .expectEarcon(Earcon.CHECK_OFF)
+      .call(doCmd('nextObject'))
+      .expectSpeech('Check box')
+      .expectEarcon(Earcon.CHECK_ON)
+      .call(doCmd('nextObject'))
+      .expectSpeech('Edit text')
+      .expectEarcon(Earcon.EDITABLE_TEXT)
 
-        // Editable text Search re-mappings are in effect.
-        .call(doCmd('toggleStickyMode'))
-        .expectSpeech('Sticky mode enabled')
-        .call(doCmd('nextObject'))
-        .expectSpeech('List box')
-        .expectEarcon(Earcon.LISTBOX)
-        .call(doCmd('nextObject'))
-        .expectSpeech('Button', 'has pop up')
-        .expectEarcon(Earcon.POP_UP_BUTTON)
-        .call(doCmd('nextObject'))
-        .expectSpeech(/Slider/)
-        .expectEarcon(Earcon.SLIDER);
+      // Editable text Search re-mappings are in effect.
+      .call(doCmd('toggleStickyMode'))
+      .expectSpeech('Sticky mode enabled')
+      .call(doCmd('nextObject'))
+      .expectSpeech('List box')
+      .expectEarcon(Earcon.LISTBOX)
+      .call(doCmd('nextObject'))
+      .expectSpeech('Button', 'has pop up')
+      .expectEarcon(Earcon.POP_UP_BUTTON)
+      .call(doCmd('nextObject'))
+      .expectSpeech(/Slider/)
+      .expectEarcon(Earcon.SLIDER);
 
-    mockFeedback.replay();
-  }.bind(this));
+  mockFeedback.replay();
 });
 
 TEST_F('ChromeVoxBackgroundTest', 'GlobsToRegExp', function() {
@@ -545,52 +537,50 @@
   })();
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'ShouldNotFocusIframe', function() {
+TEST_F('ChromeVoxBackgroundTest', 'ShouldNotFocusIframe', async function() {
   const site = `
     <iframe tabindex=0 src="data:text/html,<p>Inside</p>"></iframe>
     <button>outside</button>
   `;
-  this.runWithLoadedTree(site, function(root) {
-    const iframe = root.find({role: RoleType.IFRAME});
-    const button = root.find({role: RoleType.BUTTON});
+  const root = await this.runWithLoadedTree(site);
+  const iframe = root.find({role: RoleType.IFRAME});
+  const button = root.find({role: RoleType.BUTTON});
 
-    assertEquals('iframe', iframe.role);
-    assertEquals('button', button.role);
+  assertEquals('iframe', iframe.role);
+  assertEquals('button', button.role);
 
-    let didFocus = false;
-    iframe.addEventListener('focus', function() {
-      didFocus = true;
-    });
-    const b = ChromeVoxState.instance;
-    b.currentRange_ = cursors.Range.fromNode(button);
-    doCmd('previousElement');
-    assertFalse(didFocus);
-  }.bind(this));
+  let didFocus = false;
+  iframe.addEventListener('focus', function() {
+    didFocus = true;
+  });
+  const b = ChromeVoxState.instance;
+  b.currentRange_ = cursors.Range.fromNode(button);
+  doCmd('previousElement');
+  assertFalse(didFocus);
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'ShouldFocusLink', function() {
+TEST_F('ChromeVoxBackgroundTest', 'ShouldFocusLink', async function() {
   const site = `
     <div><a href="#">mylink</a></div>
     <button>after</button>
   `;
-  this.runWithLoadedTree(site, function(root) {
-    const link = root.find({role: RoleType.LINK});
-    const button = root.find({role: RoleType.BUTTON});
+  const root = await this.runWithLoadedTree(site);
+  const link = root.find({role: RoleType.LINK});
+  const button = root.find({role: RoleType.BUTTON});
 
-    assertEquals('link', link.role);
-    assertEquals('button', button.role);
+  assertEquals('link', link.role);
+  assertEquals('button', button.role);
 
-    const didFocus = false;
-    link.addEventListener('focus', this.newCallback(function() {
-      // Success
-    }));
-    const b = ChromeVoxState.instance;
-    b.currentRange_ = cursors.Range.fromNode(button);
-    doCmd('previousElement');
-  });
+  const didFocus = false;
+  link.addEventListener('focus', this.newCallback(function() {
+    // Success
+  }));
+  const b = ChromeVoxState.instance;
+  b.currentRange_ = cursors.Range.fromNode(button);
+  doCmd('previousElement');
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'NoisySlider', function() {
+TEST_F('ChromeVoxBackgroundTest', 'NoisySlider', async function() {
   const mockFeedback = this.createMockFeedback();
   // Slider aria-valuetext must change otherwise blink suppresses event.
   const site = `
@@ -606,21 +596,20 @@
       update();
     </script>
   `;
-  this.runWithLoadedTree(site, function(root) {
-    const go = root.find({role: RoleType.BUTTON});
-    const slider = root.find({role: RoleType.SLIDER});
-    const focusButton = go.focus.bind(go);
-    const focusSlider = slider.focus.bind(slider);
-    mockFeedback.call(focusButton)
-        .expectNextSpeechUtteranceIsNot('noisy')
-        .call(focusSlider)
-        .expectSpeech('noisy')
-        .expectSpeech('noisy')
-        .replay();
-  }.bind(this));
+  const root = await this.runWithLoadedTree(site);
+  const go = root.find({role: RoleType.BUTTON});
+  const slider = root.find({role: RoleType.SLIDER});
+  const focusButton = go.focus.bind(go);
+  const focusSlider = slider.focus.bind(slider);
+  mockFeedback.call(focusButton)
+      .expectNextSpeechUtteranceIsNot('noisy')
+      .call(focusSlider)
+      .expectSpeech('noisy')
+      .expectSpeech('noisy')
+      .replay();
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'Checkbox', function() {
+TEST_F('ChromeVoxBackgroundTest', 'Checkbox', async function() {
   const mockFeedback = this.createMockFeedback();
   const site = `
     <div id="go" role="checkbox">go</div>
@@ -637,38 +626,36 @@
       });
     </script>
   `;
-  this.runWithLoadedTree(site, function(root) {
-    const cbx = root.find({role: RoleType.CHECK_BOX});
-    const click = cbx.doDefault.bind(cbx);
-    const focus = cbx.focus.bind(cbx);
-    mockFeedback.call(focus)
-        .expectSpeech('go')
-        .expectSpeech('Check box')
-        .expectSpeech('Not checked')
-        .call(click)
-        .expectSpeech('go')
-        .expectSpeech('Check box')
-        .expectSpeech('Checked')
-        .call(click)
-        .expectSpeech('go')
-        .expectSpeech('Check box')
-        .expectSpeech('Not checked')
-        .replay();
-  });
+  const root = await this.runWithLoadedTree(site);
+  const cbx = root.find({role: RoleType.CHECK_BOX});
+  const click = cbx.doDefault.bind(cbx);
+  const focus = cbx.focus.bind(cbx);
+  mockFeedback.call(focus)
+      .expectSpeech('go')
+      .expectSpeech('Check box')
+      .expectSpeech('Not checked')
+      .call(click)
+      .expectSpeech('go')
+      .expectSpeech('Check box')
+      .expectSpeech('Checked')
+      .call(click)
+      .expectSpeech('go')
+      .expectSpeech('Check box')
+      .expectSpeech('Not checked')
+      .replay();
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'MixedCheckbox', function() {
+TEST_F('ChromeVoxBackgroundTest', 'MixedCheckbox', async function() {
   const mockFeedback = this.createMockFeedback();
   const site = '<div id="go" role="checkbox" aria-checked="mixed">go</div>';
-  this.runWithLoadedTree(site, function(root) {
-    mockFeedback.expectSpeech('go', 'Check box', 'Partially checked').replay();
-  });
+  const root = await this.runWithLoadedTree(site);
+  mockFeedback.expectSpeech('go', 'Check box', 'Partially checked').replay();
 });
 
 /** Tests navigating into and out of iframes using nextButton */
 TEST_F(
     'ChromeVoxBackgroundTest', 'ForwardNavigationThroughIframeButtons',
-    function() {
+    async function() {
       const mockFeedback = this.createMockFeedback();
 
       let running = false;
@@ -698,21 +685,20 @@
         mockFeedback.replay();
       }.bind(this);
 
-      this.runWithLoadedTree(this.iframesDoc, function(rootNode) {
-        chrome.automation.getDesktop(function(desktopNode) {
-          runTestIfIframeIsLoaded(rootNode);
+      const rootNode = await this.runWithLoadedTree(this.iframesDoc);
+      chrome.automation.getDesktop(function(desktopNode) {
+        runTestIfIframeIsLoaded(rootNode);
 
-          desktopNode.addEventListener('loadComplete', function(evt) {
-            runTestIfIframeIsLoaded(rootNode);
-          }, true);
-        });
+        desktopNode.addEventListener('loadComplete', function(evt) {
+          runTestIfIframeIsLoaded(rootNode);
+        }, true);
       });
     });
 
 /** Tests navigating into and out of iframes using nextObject */
 TEST_F(
     'ChromeVoxBackgroundTest', 'ForwardObjectNavigationThroughIframes',
-    function() {
+    async function() {
       const mockFeedback = this.createMockFeedback();
 
       let running = false;
@@ -751,18 +737,17 @@
         mockFeedback.replay();
       }.bind(this);
 
-      this.runWithLoadedTree(this.iframesDoc, function(rootNode) {
-        chrome.automation.getDesktop(function(desktopNode) {
-          runTestIfIframeIsLoaded(rootNode);
+      const rootNode = await this.runWithLoadedTree(this.iframesDoc);
+      chrome.automation.getDesktop(function(desktopNode) {
+        runTestIfIframeIsLoaded(rootNode);
 
-          desktopNode.addEventListener('loadComplete', function(evt) {
-            runTestIfIframeIsLoaded(rootNode);
-          }, true);
-        });
+        desktopNode.addEventListener('loadComplete', function(evt) {
+          runTestIfIframeIsLoaded(rootNode);
+        }, true);
       });
     });
 
-TEST_F('ChromeVoxBackgroundTest', 'SelectOptionSelected', function() {
+TEST_F('ChromeVoxBackgroundTest', 'SelectOptionSelected', async function() {
   // Undoes the ChromeVoxNextE2E call setting this to true. The doDefault action
   // should always be read.
   BaseAutomationHandler.announceActions = false;
@@ -775,28 +760,27 @@
       <option>grapefruit
     </select>
   `;
-  this.runWithLoadedTree(site, function(root) {
-    const select = root.find({role: RoleType.POP_UP_BUTTON});
-    const clickSelect = select.doDefault.bind(select);
-    const selectLastOption = () => {
-      const options = select.findAll({role: RoleType.LIST_BOX_OPTION});
-      options[options.length - 1].doDefault();
-    };
+  const root = await this.runWithLoadedTree(site);
+  const select = root.find({role: RoleType.POP_UP_BUTTON});
+  const clickSelect = select.doDefault.bind(select);
+  const selectLastOption = () => {
+    const options = select.findAll({role: RoleType.LIST_BOX_OPTION});
+    options[options.length - 1].doDefault();
+  };
 
-    mockFeedback.call(doCmd('nextObject'))
-        .expectSpeech('Button', 'Press Search+Space to activate')
-        .call(clickSelect)
-        .expectSpeech('apple')
-        .expectSpeech('Button')
-        .expectSpeech('Expanded')
-        .call(selectLastOption)
-        .expectNextSpeechUtteranceIsNot('apple')
-        .expectSpeech('grapefruit')
-        .replay();
-  });
+  mockFeedback.call(doCmd('nextObject'))
+      .expectSpeech('Button', 'Press Search+Space to activate')
+      .call(clickSelect)
+      .expectSpeech('apple')
+      .expectSpeech('Button')
+      .expectSpeech('Expanded')
+      .call(selectLastOption)
+      .expectNextSpeechUtteranceIsNot('apple')
+      .expectSpeech('grapefruit')
+      .replay();
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'ToggleButton', function() {
+TEST_F('ChromeVoxBackgroundTest', 'ToggleButton', async function() {
   const mockFeedback = this.createMockFeedback();
   const site = `
     <div aria-pressed="mixed" role="button">boldface</div>
@@ -804,58 +788,55 @@
     <div aria-pressed="false" role="button">cancel</div>
     <div aria-pressed role="button">close</div>
   `;
-  this.runWithLoadedTree(site, function(root) {
-    const b = ChromeVoxState.instance;
-    const move = doCmd('nextObject');
-    mockFeedback.call(move)
-        .expectSpeech('boldface')
-        .expectSpeech('Toggle Button')
-        .expectSpeech('Partially pressed')
+  const root = await this.runWithLoadedTree(site);
+  const b = ChromeVoxState.instance;
+  const move = doCmd('nextObject');
+  mockFeedback.call(move)
+      .expectSpeech('boldface')
+      .expectSpeech('Toggle Button')
+      .expectSpeech('Partially pressed')
 
-        .call(move)
-        .expectSpeech('ok')
-        .expectSpeech('Toggle Button')
-        .expectSpeech('Pressed')
+      .call(move)
+      .expectSpeech('ok')
+      .expectSpeech('Toggle Button')
+      .expectSpeech('Pressed')
 
-        .call(move)
-        .expectSpeech('cancel')
-        .expectSpeech('Toggle Button')
-        .expectSpeech('Not pressed')
+      .call(move)
+      .expectSpeech('cancel')
+      .expectSpeech('Toggle Button')
+      .expectSpeech('Not pressed')
 
-        .call(move)
-        .expectSpeech('close')
-        .expectSpeech('Button')
+      .call(move)
+      .expectSpeech('close')
+      .expectSpeech('Button')
 
-        .replay();
-  });
+      .replay();
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'EditText', function() {
+TEST_F('ChromeVoxBackgroundTest', 'EditText', async function() {
   const mockFeedback = this.createMockFeedback();
   const site = `
     <input type="text"></input>
     <input role="combobox" type="text"></input>
   `;
-  this.runWithLoadedTree(site, function(root) {
-    const nextEditText = doCmd('nextEditText');
-    const previousEditText = doCmd('previousEditText');
-    mockFeedback.call(nextEditText)
-        .expectSpeech('Combo box')
-        .call(previousEditText)
-        .expectSpeech('Edit text')
-        .replay();
-  });
+  const root = await this.runWithLoadedTree(site);
+  const nextEditText = doCmd('nextEditText');
+  const previousEditText = doCmd('previousEditText');
+  mockFeedback.call(nextEditText)
+      .expectSpeech('Combo box')
+      .call(previousEditText)
+      .expectSpeech('Edit text')
+      .replay();
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'ComboBox', function() {
+TEST_F('ChromeVoxBackgroundTest', 'ComboBox', async function() {
   const mockFeedback = this.createMockFeedback();
-  this.runWithLoadedTree(this.comboBoxDoc, function() {
-    mockFeedback.expectSpeech('Edit text', 'Choose an item', 'Combo box')
-        .replay();
-  });
+  await this.runWithLoadedTree(this.comboBoxDoc);
+  mockFeedback.expectSpeech('Edit text', 'Choose an item', 'Combo box')
+      .replay();
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'BackwardForwardSync', function() {
+TEST_F('ChromeVoxBackgroundTest', 'BackwardForwardSync', async function() {
   const mockFeedback = this.createMockFeedback();
   const site = `
     <div aria-label="Group" role="group" tabindex=0>
@@ -867,105 +848,101 @@
       </li>
     </ul>
   `;
-  this.runWithLoadedTree(site, function(root) {
-    const listItem = root.find({role: RoleType.LIST_ITEM});
+  const root = await this.runWithLoadedTree(site);
+  const listItem = root.find({role: RoleType.LIST_ITEM});
 
-    mockFeedback.call(listItem.focus.bind(listItem))
-        .expectSpeech('ok', 'List item')
-        .call(this.doCmd('nextObject'))
-        .expectSpeech('\u2022 ')  // bullet
-        .call(this.doCmd('nextObject'))
-        .expectSpeech('Button')
-        .call(this.doCmd('previousObject'))
-        .expectSpeech('\u2022 ')  // bullet
-        .call(this.doCmd('previousObject'))
-        .expectSpeech('List item')
-        .call(this.doCmd('previousObject'))
-        .expectSpeech('Edit text')
-        .call(this.doCmd('previousObject'))
-        .expectSpeech('Group')
-        .replay();
-  });
+  mockFeedback.call(listItem.focus.bind(listItem))
+      .expectSpeech('ok', 'List item')
+      .call(this.doCmd('nextObject'))
+      .expectSpeech('\u2022 ')  // bullet
+      .call(this.doCmd('nextObject'))
+      .expectSpeech('Button')
+      .call(this.doCmd('previousObject'))
+      .expectSpeech('\u2022 ')  // bullet
+      .call(this.doCmd('previousObject'))
+      .expectSpeech('List item')
+      .call(this.doCmd('previousObject'))
+      .expectSpeech('Edit text')
+      .call(this.doCmd('previousObject'))
+      .expectSpeech('Group')
+      .replay();
 });
 
 /** Tests that navigation works when the current object disappears. */
-TEST_F('ChromeVoxBackgroundTest', 'DisappearingObject', function() {
+TEST_F('ChromeVoxBackgroundTest', 'DisappearingObject', async function() {
   const mockFeedback = this.createMockFeedback();
-  this.runWithLoadedTree(this.disappearingObjectDoc, function(rootNode) {
-    const deleteButton =
-        rootNode.find({role: RoleType.BUTTON, attributes: {name: 'Delete'}});
-    const pressDelete = deleteButton.doDefault.bind(deleteButton);
-    mockFeedback.expectSpeech('start').expectBraille('start');
+  const rootNode = await this.runWithLoadedTree(this.disappearingObjectDoc);
+  const deleteButton =
+      rootNode.find({role: RoleType.BUTTON, attributes: {name: 'Delete'}});
+  const pressDelete = deleteButton.doDefault.bind(deleteButton);
+  mockFeedback.expectSpeech('start').expectBraille('start');
 
-    mockFeedback.call(doCmd('nextObject'))
-        .expectSpeech('Before1')
-        .call(doCmd('nextObject'))
-        .expectSpeech('Before2')
-        .call(doCmd('nextObject'))
-        .expectSpeech('Before3')
-        .call(doCmd('nextObject'))
-        .expectSpeech('Disappearing')
-        .call(pressDelete)
-        .expectSpeech('Deleted')
-        .call(doCmd('nextObject'))
-        .expectSpeech('After1')
-        .call(doCmd('nextObject'))
-        .expectSpeech('After2')
-        .call(doCmd('previousObject'))
-        .expectSpeech('After1')
-        .call(doCmd('dumpTree'));
+  mockFeedback.call(doCmd('nextObject'))
+      .expectSpeech('Before1')
+      .call(doCmd('nextObject'))
+      .expectSpeech('Before2')
+      .call(doCmd('nextObject'))
+      .expectSpeech('Before3')
+      .call(doCmd('nextObject'))
+      .expectSpeech('Disappearing')
+      .call(pressDelete)
+      .expectSpeech('Deleted')
+      .call(doCmd('nextObject'))
+      .expectSpeech('After1')
+      .call(doCmd('nextObject'))
+      .expectSpeech('After2')
+      .call(doCmd('previousObject'))
+      .expectSpeech('After1')
+      .call(doCmd('dumpTree'));
 
-    /*
-        // This is broken by cl/1260523 making tree updating (more)
-       asynchronous.
-        // TODO(aboxhall/dtseng): Add a function to wait for next tree update?
-        mockFeedback
-            .call(doCmd('previousObject'))
-            .expectSpeech('Before3');
-    */
+  /*
+      // This is broken by cl/1260523 making tree updating (more)
+     asynchronous.
+      // TODO(aboxhall/dtseng): Add a function to wait for next tree update?
+      mockFeedback
+          .call(doCmd('previousObject'))
+          .expectSpeech('Before3');
+  */
 
-    mockFeedback.replay();
-  });
+  mockFeedback.replay();
 });
 
 /** Tests that focus jumps to details properly when indicated. */
-TEST_F('ChromeVoxBackgroundTest', 'JumpToDetails', function() {
+TEST_F('ChromeVoxBackgroundTest', 'JumpToDetails', async function() {
   const mockFeedback = this.createMockFeedback();
-  this.runWithLoadedTree(this.detailsDoc, function(rootNode) {
-    mockFeedback.call(doCmd('jumpToDetails')).expectSpeech('Details');
-    mockFeedback.replay();
-  });
+  const rootNode = await this.runWithLoadedTree(this.detailsDoc);
+  mockFeedback.call(doCmd('jumpToDetails')).expectSpeech('Details');
+  mockFeedback.replay();
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'ButtonNameValueDescription', function() {
-  const mockFeedback = this.createMockFeedback();
-  const site = '<input type="submit" aria-label="foo" value="foo"></input>';
-  this.runWithLoadedTree(site, function(root) {
-    const btn = root.find({role: RoleType.BUTTON});
-    mockFeedback.call(btn.focus.bind(btn))
-        .expectSpeech('foo')
-        .expectSpeech('Button')
-        .replay();
-  });
-});
+TEST_F(
+    'ChromeVoxBackgroundTest', 'ButtonNameValueDescription', async function() {
+      const mockFeedback = this.createMockFeedback();
+      const site = '<input type="submit" aria-label="foo" value="foo"></input>';
+      const root = await this.runWithLoadedTree(site);
+      const btn = root.find({role: RoleType.BUTTON});
+      mockFeedback.call(btn.focus.bind(btn))
+          .expectSpeech('foo')
+          .expectSpeech('Button')
+          .replay();
+    });
 
-TEST_F('ChromeVoxBackgroundTest', 'NameFromHeadingLink', function() {
+TEST_F('ChromeVoxBackgroundTest', 'NameFromHeadingLink', async function() {
   const mockFeedback = this.createMockFeedback();
   const site = `
     <p>before</p>
     <h1><a href="google.com">go</a><p>here</p></h1>
   `;
-  this.runWithLoadedTree(site, function(root) {
-    const link = root.find({role: RoleType.LINK});
-    mockFeedback.call(link.focus.bind(link))
-        .expectSpeech('go')
-        .expectSpeech('Link')
-        .expectSpeech('Heading 1')
-        .replay();
-  });
+  const root = await this.runWithLoadedTree(site);
+  const link = root.find({role: RoleType.LINK});
+  mockFeedback.call(link.focus.bind(link))
+      .expectSpeech('go')
+      .expectSpeech('Link')
+      .expectSpeech('Heading 1')
+      .replay();
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'OptionChildIndexCount', function() {
+TEST_F('ChromeVoxBackgroundTest', 'OptionChildIndexCount', async function() {
   const mockFeedback = this.createMockFeedback();
   const site = `
     <div role="listbox">
@@ -975,57 +952,56 @@
     </div>
   `;
 
-  this.runWithLoadedTree(site, function(root) {
-    // Select first child of the list box, similar to what happens if navigated
-    // by Tab.
-    const firstChild = root.find({role: RoleType.PARAGRAPH});
-    mockFeedback
-        .call(
-            () => ChromeVoxState.instance.setCurrentRange(
-                cursors.Range.fromNode(firstChild)))
-        .call(doCmd('nextObject'))
-        .expectSpeech('List box')
-        .expectSpeech('Fruits')
-        .call(doCmd('nextObject'))
-        .expectSpeech('apple')
-        .expectSpeech(' 1 of 2 ')
-        .call(doCmd('nextObject'))
-        .expectSpeech('banana')
-        .expectSpeech(' 2 of 2 ')
-        .replay();
-  });
+  const root = await this.runWithLoadedTree(site);
+  // Select first child of the list box, similar to what happens if navigated
+  // by Tab.
+  const firstChild = root.find({role: RoleType.PARAGRAPH});
+  mockFeedback
+      .call(
+          () => ChromeVoxState.instance.setCurrentRange(
+              cursors.Range.fromNode(firstChild)))
+      .call(doCmd('nextObject'))
+      .expectSpeech('List box')
+      .expectSpeech('Fruits')
+      .call(doCmd('nextObject'))
+      .expectSpeech('apple')
+      .expectSpeech(' 1 of 2 ')
+      .call(doCmd('nextObject'))
+      .expectSpeech('banana')
+      .expectSpeech(' 2 of 2 ')
+      .replay();
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'ListMarkerIsIgnored', function() {
+TEST_F('ChromeVoxBackgroundTest', 'ListMarkerIsIgnored', async function() {
   const mockFeedback = this.createMockFeedback();
-  this.runWithLoadedTree('<ul><li>apple</ul>', function(root) {
-    mockFeedback.call(doCmd('nextObject'))
-        .expectNextSpeechUtteranceIsNot('listMarker')
-        .expectSpeech('\u2022 apple')  // bullet apple
-        .replay();
-  });
+  const root = await this.runWithLoadedTree('<ul><li>apple</ul>');
+  mockFeedback.call(doCmd('nextObject'))
+      .expectNextSpeechUtteranceIsNot('listMarker')
+      .expectSpeech('\u2022 apple')  // bullet apple
+      .replay();
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'SymetricComplexHeading', function() {
+TEST_F('ChromeVoxBackgroundTest', 'SymetricComplexHeading', async function() {
   const mockFeedback = this.createMockFeedback();
   const site = `
     <h4><p>NW</p><p>NE</p></h4>
     <h4><p>SW</p><p>SE</p></h4>
   `;
-  this.runWithLoadedTree(site, function(root) {
-    mockFeedback.call(doCmd('nextHeading'))
-        .expectNextSpeechUtteranceIsNot('NE')
-        .expectSpeech('NW')
-        .call(doCmd('previousHeading'))
-        .expectNextSpeechUtteranceIsNot('NE')
-        .expectSpeech('NW')
-        .replay();
-  });
+  const root = await this.runWithLoadedTree(site);
+  mockFeedback.call(doCmd('nextHeading'))
+      .expectNextSpeechUtteranceIsNot('NE')
+      .expectSpeech('NW')
+      .call(doCmd('previousHeading'))
+      .expectNextSpeechUtteranceIsNot('NE')
+      .expectSpeech('NW')
+      .replay();
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'ContentEditableJumpSyncsRange', function() {
-  const mockFeedback = this.createMockFeedback();
-  const site = `
+TEST_F(
+    'ChromeVoxBackgroundTest', 'ContentEditableJumpSyncsRange',
+    async function() {
+      const mockFeedback = this.createMockFeedback();
+      const site = `
     <p>start</p>
     <div contenteditable>
       <h1>Top News</h1>
@@ -1033,61 +1009,59 @@
       <h1>Sports</h1>
     </div>
   `;
-  this.runWithLoadedTree(site, function(root) {
-    const assertRangeHasText = function(text) {
-      return function() {
-        assertEquals(
-            text, ChromeVoxState.instance.getCurrentRange().start.node.name);
+      const root = await this.runWithLoadedTree(site);
+      const assertRangeHasText = function(text) {
+        return function() {
+          assertEquals(
+              text, ChromeVoxState.instance.getCurrentRange().start.node.name);
+        };
       };
-    };
 
-    mockFeedback.call(doCmd('nextEditText'))
-        .expectSpeech('Top News Most Popular Sports')
-        .call(doCmd('nextHeading'))
-        .expectSpeech('Top News')
-        .call(assertRangeHasText('Top News'))
-        .call(doCmd('nextHeading'))
-        .expectSpeech('Most Popular')
-        .call(assertRangeHasText('Most Popular'))
-        .call(doCmd('nextHeading'))
-        .expectSpeech('Sports')
-        .call(assertRangeHasText('Sports'))
-        .replay();
-  });
-});
+      mockFeedback.call(doCmd('nextEditText'))
+          .expectSpeech('Top News Most Popular Sports')
+          .call(doCmd('nextHeading'))
+          .expectSpeech('Top News')
+          .call(assertRangeHasText('Top News'))
+          .call(doCmd('nextHeading'))
+          .expectSpeech('Most Popular')
+          .call(assertRangeHasText('Most Popular'))
+          .call(doCmd('nextHeading'))
+          .expectSpeech('Sports')
+          .call(assertRangeHasText('Sports'))
+          .replay();
+    });
 
-TEST_F('ChromeVoxBackgroundTest', 'Selection', function() {
+TEST_F('ChromeVoxBackgroundTest', 'Selection', async function() {
   const mockFeedback = this.createMockFeedback();
   const site = `
     <p>simple</p>
     <p>doc</p>
   `;
-  this.runWithLoadedTree(site, function(root) {
-    // Fakes a toggleSelection command.
-    root.addEventListener(EventType.DOCUMENT_SELECTION_CHANGED, function() {
-      if (root.focusObject.name === 'simple' && root.focusOffset === 3) {
-        CommandHandlerInterface.instance.onCommand('toggleSelection');
-      }
-    }, true);
+  const root = await this.runWithLoadedTree(site);
+  // Fakes a toggleSelection command.
+  root.addEventListener(EventType.DOCUMENT_SELECTION_CHANGED, function() {
+    if (root.focusObject.name === 'simple' && root.focusOffset === 3) {
+      CommandHandlerInterface.instance.onCommand('toggleSelection');
+    }
+  }, true);
 
-    mockFeedback.call(doCmd('toggleSelection'))
-        .expectSpeech('simple', 'selected')
-        .call(doCmd('nextObject'))
-        .expectSpeech('doc', 'selected')
-        .call(doCmd('previousObject'))
-        .expectSpeech('doc', 'unselected')
-        .call(doCmd('nextCharacter'))
-        .expectSpeech('i', 'selected')
-        .call(doCmd('previousCharacter'))
-        .expectSpeech('i', 'unselected')
-        .call(doCmd('nextCharacter'))
-        .call(doCmd('nextCharacter'))
-        .expectSpeech('End selection', 'sim')
-        .replay();
-  });
+  mockFeedback.call(doCmd('toggleSelection'))
+      .expectSpeech('simple', 'selected')
+      .call(doCmd('nextObject'))
+      .expectSpeech('doc', 'selected')
+      .call(doCmd('previousObject'))
+      .expectSpeech('doc', 'unselected')
+      .call(doCmd('nextCharacter'))
+      .expectSpeech('i', 'selected')
+      .call(doCmd('previousCharacter'))
+      .expectSpeech('i', 'unselected')
+      .call(doCmd('nextCharacter'))
+      .call(doCmd('nextCharacter'))
+      .expectSpeech('End selection', 'sim')
+      .replay();
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'BasicTableCommands', function() {
+TEST_F('ChromeVoxBackgroundTest', 'BasicTableCommands', async function() {
   const mockFeedback = this.createMockFeedback();
   const site = `
   <table border=1>
@@ -1095,70 +1069,69 @@
     <tr><td>Dan</td><td>Mr</td><td>666 Elm Street</td><td>212 222 5555</td></tr>
   </table>
   `;
-  this.runWithLoadedTree(site, function(root) {
-    mockFeedback.call(doCmd('nextRow'))
-        .expectSpeech('Dan', 'row 2 column 1')
-        .call(doCmd('previousRow'))
-        .expectSpeech('name', 'row 1 column 1')
-        .call(doCmd('previousRow'))
-        .expectSpeech('No cell above')
-        .call(doCmd('nextCol'))
-        .expectSpeech('title', 'row 1 column 2')
-        .call(doCmd('nextRow'))
-        .expectSpeech('Mr', 'row 2 column 2')
-        .call(doCmd('previousRow'))
-        .expectSpeech('title', 'row 1 column 2')
-        .call(doCmd('nextCol'))
-        .expectSpeech('address', 'row 1 column 3')
-        .call(doCmd('nextCol'))
-        .expectSpeech('phone', 'row 1 column 4')
-        .call(doCmd('nextCol'))
-        .expectSpeech('No cell right')
-        .call(doCmd('previousRow'))
-        .expectSpeech('No cell above')
-        .call(doCmd('nextRow'))
-        .expectSpeech('212 222 5555', 'row 2 column 4')
-        .call(doCmd('nextRow'))
-        .expectSpeech('No cell below')
-        .call(doCmd('nextCol'))
-        .expectSpeech('No cell right')
-        .call(doCmd('previousCol'))
-        .expectSpeech('666 Elm Street', 'row 2 column 3')
-        .call(doCmd('previousCol'))
-        .expectSpeech('Mr', 'row 2 column 2')
+  const root = await this.runWithLoadedTree(site);
+  mockFeedback.call(doCmd('nextRow'))
+      .expectSpeech('Dan', 'row 2 column 1')
+      .call(doCmd('previousRow'))
+      .expectSpeech('name', 'row 1 column 1')
+      .call(doCmd('previousRow'))
+      .expectSpeech('No cell above')
+      .call(doCmd('nextCol'))
+      .expectSpeech('title', 'row 1 column 2')
+      .call(doCmd('nextRow'))
+      .expectSpeech('Mr', 'row 2 column 2')
+      .call(doCmd('previousRow'))
+      .expectSpeech('title', 'row 1 column 2')
+      .call(doCmd('nextCol'))
+      .expectSpeech('address', 'row 1 column 3')
+      .call(doCmd('nextCol'))
+      .expectSpeech('phone', 'row 1 column 4')
+      .call(doCmd('nextCol'))
+      .expectSpeech('No cell right')
+      .call(doCmd('previousRow'))
+      .expectSpeech('No cell above')
+      .call(doCmd('nextRow'))
+      .expectSpeech('212 222 5555', 'row 2 column 4')
+      .call(doCmd('nextRow'))
+      .expectSpeech('No cell below')
+      .call(doCmd('nextCol'))
+      .expectSpeech('No cell right')
+      .call(doCmd('previousCol'))
+      .expectSpeech('666 Elm Street', 'row 2 column 3')
+      .call(doCmd('previousCol'))
+      .expectSpeech('Mr', 'row 2 column 2')
 
-        .call(doCmd('goToRowLastCell'))
-        .expectSpeech('212 222 5555', 'row 2 column 4')
-        .call(doCmd('goToRowLastCell'))
-        .expectSpeech('212 222 5555')
-        .call(doCmd('goToRowFirstCell'))
-        .expectSpeech('Dan', 'row 2 column 1')
-        .call(doCmd('goToRowFirstCell'))
-        .expectSpeech('Dan')
+      .call(doCmd('goToRowLastCell'))
+      .expectSpeech('212 222 5555', 'row 2 column 4')
+      .call(doCmd('goToRowLastCell'))
+      .expectSpeech('212 222 5555')
+      .call(doCmd('goToRowFirstCell'))
+      .expectSpeech('Dan', 'row 2 column 1')
+      .call(doCmd('goToRowFirstCell'))
+      .expectSpeech('Dan')
 
-        .call(doCmd('goToColFirstCell'))
-        .expectSpeech('name', 'row 1 column 1')
-        .call(doCmd('goToColFirstCell'))
-        .expectSpeech('name')
-        .call(doCmd('goToColLastCell'))
-        .expectSpeech('Dan', 'row 2 column 1')
-        .call(doCmd('goToColLastCell'))
-        .expectSpeech('Dan')
+      .call(doCmd('goToColFirstCell'))
+      .expectSpeech('name', 'row 1 column 1')
+      .call(doCmd('goToColFirstCell'))
+      .expectSpeech('name')
+      .call(doCmd('goToColLastCell'))
+      .expectSpeech('Dan', 'row 2 column 1')
+      .call(doCmd('goToColLastCell'))
+      .expectSpeech('Dan')
 
-        .call(doCmd('goToLastCell'))
-        .expectSpeech('212 222 5555', 'row 2 column 4')
-        .call(doCmd('goToLastCell'))
-        .expectSpeech('212 222 5555')
-        .call(doCmd('goToFirstCell'))
-        .expectSpeech('name', 'row 1 column 1')
-        .call(doCmd('goToFirstCell'))
-        .expectSpeech('name')
+      .call(doCmd('goToLastCell'))
+      .expectSpeech('212 222 5555', 'row 2 column 4')
+      .call(doCmd('goToLastCell'))
+      .expectSpeech('212 222 5555')
+      .call(doCmd('goToFirstCell'))
+      .expectSpeech('name', 'row 1 column 1')
+      .call(doCmd('goToFirstCell'))
+      .expectSpeech('name')
 
-        .replay();
-  });
+      .replay();
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'MissingTableCells', function() {
+TEST_F('ChromeVoxBackgroundTest', 'MissingTableCells', async function() {
   const mockFeedback = this.createMockFeedback();
   const site = `
   <table border=1>
@@ -1167,141 +1140,133 @@
     <tr><td>f</td></tr>
   </table>
   `;
-  this.runWithLoadedTree(site, function(root) {
-    mockFeedback.call(doCmd('goToRowLastCell'))
-        .expectSpeech('c', 'row 1 column 3')
-        .call(doCmd('goToRowLastCell'))
-        .expectSpeech('c')
-        .call(doCmd('goToRowFirstCell'))
-        .expectSpeech('a', 'row 1 column 1')
-        .call(doCmd('goToRowFirstCell'))
-        .expectSpeech('a')
+  const root = await this.runWithLoadedTree(site);
+  mockFeedback.call(doCmd('goToRowLastCell'))
+      .expectSpeech('c', 'row 1 column 3')
+      .call(doCmd('goToRowLastCell'))
+      .expectSpeech('c')
+      .call(doCmd('goToRowFirstCell'))
+      .expectSpeech('a', 'row 1 column 1')
+      .call(doCmd('goToRowFirstCell'))
+      .expectSpeech('a')
 
-        .call(doCmd('nextCol'))
-        .expectSpeech('b', 'row 1 column 2')
+      .call(doCmd('nextCol'))
+      .expectSpeech('b', 'row 1 column 2')
 
-        .call(doCmd('goToColLastCell'))
-        .expectSpeech('e', 'row 2 column 2')
-        .call(doCmd('goToColLastCell'))
-        .expectSpeech('e')
-        .call(doCmd('goToColFirstCell'))
-        .expectSpeech('b', 'row 1 column 2')
-        .call(doCmd('goToColFirstCell'))
-        .expectSpeech('b')
+      .call(doCmd('goToColLastCell'))
+      .expectSpeech('e', 'row 2 column 2')
+      .call(doCmd('goToColLastCell'))
+      .expectSpeech('e')
+      .call(doCmd('goToColFirstCell'))
+      .expectSpeech('b', 'row 1 column 2')
+      .call(doCmd('goToColFirstCell'))
+      .expectSpeech('b')
 
-        .call(doCmd('goToFirstCell'))
-        .expectSpeech('a', 'row 1 column 1')
-        .call(doCmd('goToFirstCell'))
-        .expectSpeech('a')
-        .call(doCmd('goToLastCell'))
-        .expectSpeech('f', 'row 3 column 1')
-        .call(doCmd('goToLastCell'))
-        .expectSpeech('f')
-        .replay();
-  });
+      .call(doCmd('goToFirstCell'))
+      .expectSpeech('a', 'row 1 column 1')
+      .call(doCmd('goToFirstCell'))
+      .expectSpeech('a')
+      .call(doCmd('goToLastCell'))
+      .expectSpeech('f', 'row 3 column 1')
+      .call(doCmd('goToLastCell'))
+      .expectSpeech('f')
+      .replay();
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'DisabledState', function() {
+TEST_F('ChromeVoxBackgroundTest', 'DisabledState', async function() {
   const mockFeedback = this.createMockFeedback();
   const site = '<button aria-disabled="true">ok</button>';
-  this.runWithLoadedTree(site, function(root) {
-    mockFeedback.expectSpeech('ok', 'Disabled', 'Button').replay();
-  });
+  const root = await this.runWithLoadedTree(site);
+  mockFeedback.expectSpeech('ok', 'Disabled', 'Button').replay();
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'HeadingLevels', function() {
+TEST_F('ChromeVoxBackgroundTest', 'HeadingLevels', async function() {
   const mockFeedback = this.createMockFeedback();
   const site = `
     <h1>1</h1><h2>2</h2><h3>3</h3><h4>4</h4><h5>5</h5><h6>6</h6>
   `;
-  this.runWithLoadedTree(site, function(root) {
-    const makeLevelAssertions = function(level) {
-      mockFeedback.call(doCmd('nextHeading' + level))
-          .expectSpeech('Heading ' + level)
-          .call(doCmd('nextHeading' + level))
-          .expectEarcon('wrap')
-          .call(doCmd('previousHeading' + level))
-          .expectEarcon('wrap');
-    };
-    for (let i = 1; i <= 6; i++) {
-      makeLevelAssertions(i);
-    }
-    mockFeedback.replay();
-  });
+  const root = await this.runWithLoadedTree(site);
+  const makeLevelAssertions = function(level) {
+    mockFeedback.call(doCmd('nextHeading' + level))
+        .expectSpeech('Heading ' + level)
+        .call(doCmd('nextHeading' + level))
+        .expectEarcon('wrap')
+        .call(doCmd('previousHeading' + level))
+        .expectEarcon('wrap');
+  };
+  for (let i = 1; i <= 6; i++) {
+    makeLevelAssertions(i);
+  }
+  mockFeedback.replay();
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'EditableNavigation', function() {
+TEST_F('ChromeVoxBackgroundTest', 'EditableNavigation', async function() {
   const mockFeedback = this.createMockFeedback();
   const site = `
     <div contenteditable>this is a test</div>
   `;
-  this.runWithLoadedTree(site, function(root) {
-    mockFeedback.call(doCmd('nextObject'))
-        .expectSpeech('this is a test')
-        .call(doCmd('nextObject'))
-        .expectSpeech('this is a test')
-        .call(doCmd('nextWord'))
-        .expectSpeech('is')
-        .call(doCmd('nextWord'))
-        .expectSpeech('a')
-        .call(doCmd('nextWord'))
-        .expectSpeech('test')
-        .replay();
-  });
+  const root = await this.runWithLoadedTree(site);
+  mockFeedback.call(doCmd('nextObject'))
+      .expectSpeech('this is a test')
+      .call(doCmd('nextObject'))
+      .expectSpeech('this is a test')
+      .call(doCmd('nextWord'))
+      .expectSpeech('is')
+      .call(doCmd('nextWord'))
+      .expectSpeech('a')
+      .call(doCmd('nextWord'))
+      .expectSpeech('test')
+      .replay();
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'NavigationMovesFocus', function() {
+TEST_F('ChromeVoxBackgroundTest', 'NavigationMovesFocus', async function() {
   const site = `
     <p>start</p>
     <input type="text"></input>
   `;
-  this.runWithLoadedTree(site, function(root) {
-    this.listenOnce(
-        root.find({role: RoleType.TEXT_FIELD}), 'focus', function(e) {
-          const focus = ChromeVoxState.instance.currentRange.start.node;
-          assertEquals(RoleType.TEXT_FIELD, focus.role);
-          assertTrue(focus.state.focused);
-        });
-    doCmd('nextEditText')();
+  const root = await this.runWithLoadedTree(site);
+  this.listenOnce(root.find({role: RoleType.TEXT_FIELD}), 'focus', function(e) {
+    const focus = ChromeVoxState.instance.currentRange.start.node;
+    assertEquals(RoleType.TEXT_FIELD, focus.role);
+    assertTrue(focus.state.focused);
   });
+  doCmd('nextEditText')();
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'BrailleCaretNavigation', function() {
+TEST_F('ChromeVoxBackgroundTest', 'BrailleCaretNavigation', async function() {
   const mockFeedback = this.createMockFeedback();
   const site = `
     <p>This is a<em>test</em> of inline braille<br>with a second line</p>
   `;
-  this.runWithLoadedTree(site, function(root) {
-    const text = 'This is a';
-    mockFeedback.call(doCmd('nextCharacter'))
-        .expectBraille(text, {startIndex: 1, endIndex: 2})  // h
-        .call(doCmd('nextCharacter'))
-        .expectBraille(text, {startIndex: 2, endIndex: 3})  // i
-        .call(doCmd('nextWord'))
-        .expectBraille(text, {startIndex: 5, endIndex: 7})  // is
-        .call(doCmd('previousWord'))
-        .expectBraille(text, {startIndex: 0, endIndex: 4})  // This
-        .call(doCmd('nextLine'))
-        // Ensure nothing is selected when the range covers the entire line.
-        .expectBraille('with a second line', {startIndex: -1, endIndex: -1})
-        .replay();
-  });
+  const root = await this.runWithLoadedTree(site);
+  const text = 'This is a';
+  mockFeedback.call(doCmd('nextCharacter'))
+      .expectBraille(text, {startIndex: 1, endIndex: 2})  // h
+      .call(doCmd('nextCharacter'))
+      .expectBraille(text, {startIndex: 2, endIndex: 3})  // i
+      .call(doCmd('nextWord'))
+      .expectBraille(text, {startIndex: 5, endIndex: 7})  // is
+      .call(doCmd('previousWord'))
+      .expectBraille(text, {startIndex: 0, endIndex: 4})  // This
+      .call(doCmd('nextLine'))
+      // Ensure nothing is selected when the range covers the entire line.
+      .expectBraille('with a second line', {startIndex: -1, endIndex: -1})
+      .replay();
 });
 
 // This tests ChromeVox's special support for following an in-page link
 // if you force-click on it. Compare with InPageLinks, below.
-TEST_F('ChromeVoxBackgroundTest', 'ForceClickInPageLinks', function() {
+TEST_F('ChromeVoxBackgroundTest', 'ForceClickInPageLinks', async function() {
   const mockFeedback = this.createMockFeedback();
   const site = `
     <a href="#there">hi</a>
     <button id="there">there</button>
   `;
-  this.runWithLoadedTree(site, function(root) {
-    mockFeedback.expectSpeech('hi', 'Internal link')
-        .call(doCmd('forceClickOnCurrentItem'))
-        .expectSpeech('there', 'Button')
-        .replay();
-  });
+  const root = await this.runWithLoadedTree(site);
+  mockFeedback.expectSpeech('hi', 'Internal link')
+      .call(doCmd('forceClickOnCurrentItem'))
+      .expectSpeech('there', 'Button')
+      .replay();
 });
 
 // This tests ChromeVox's handling of the scrolledToAnchor event, which is
@@ -1312,22 +1277,23 @@
 // Note: this test needs the test server running because the browser
 // does not follow same-page links on data urls (because it modifies the
 // url fragment, and any change to the url is disallowed for a data url).
-TEST_F('ChromeVoxBackgroundTestWithTestServer', 'InPageLinks', function() {
-  const mockFeedback = this.createMockFeedback();
-  this.runWithLoadedTree(undefined, function(root) {
-    mockFeedback.call(doCmd('nextObject'))
-        .expectSpeech('Jump', 'Internal link')
-        .call(press(KeyCode.RETURN))
-        .expectSpeech('Found It')
-        .call(doCmd('nextHeading'))
-        .expectSpeech('Continue Here', 'Heading 2')
-        .replay();
-  }.bind(this), {
-    url: `${testRunnerParams.testServerBaseUrl}accessibility/in_page_links.html`
-  });
-});
+TEST_F(
+    'ChromeVoxBackgroundTestWithTestServer', 'InPageLinks', async function() {
+      const mockFeedback = this.createMockFeedback();
+      const root = await this.runWithLoadedTree(undefined, {
+        url: `${
+            testRunnerParams.testServerBaseUrl}accessibility/in_page_links.html`
+      });
+      mockFeedback.call(doCmd('nextObject'))
+          .expectSpeech('Jump', 'Internal link')
+          .call(press(KeyCode.RETURN))
+          .expectSpeech('Found It')
+          .call(doCmd('nextHeading'))
+          .expectSpeech('Continue Here', 'Heading 2')
+          .replay();
+    });
 
-TEST_F('ChromeVoxBackgroundTest', 'ListItem', function() {
+TEST_F('ChromeVoxBackgroundTest', 'ListItem', async function() {
   this.resetContextualOutput();
   const mockFeedback = this.createMockFeedback();
   const site = `
@@ -1335,130 +1301,125 @@
     <ul><li>apple<li>grape<li>banana</ul>
     <ol><li>pork<li>beef<li>chicken</ol>
   `;
-  this.runWithLoadedTree(site, function(root) {
-    mockFeedback.call(doCmd('nextLine'))
-        .expectSpeech('\u2022 apple', 'List item')
-        .expectBraille('\u2022 apple lstitm lst +3')
-        .call(doCmd('nextLine'))
-        .expectSpeech('\u2022 grape', 'List item')
-        .expectBraille('\u2022 grape lstitm')
-        .call(doCmd('nextLine'))
-        .expectSpeech('\u2022 banana', 'List item')
-        .expectBraille('\u2022 banana lstitm lst end')
+  const root = await this.runWithLoadedTree(site);
+  mockFeedback.call(doCmd('nextLine'))
+      .expectSpeech('\u2022 apple', 'List item')
+      .expectBraille('\u2022 apple lstitm lst +3')
+      .call(doCmd('nextLine'))
+      .expectSpeech('\u2022 grape', 'List item')
+      .expectBraille('\u2022 grape lstitm')
+      .call(doCmd('nextLine'))
+      .expectSpeech('\u2022 banana', 'List item')
+      .expectBraille('\u2022 banana lstitm lst end')
 
-        // Object nav should be the same.
-        .call(doCmd('nextObject'))
-        .expectSpeech('1. pork', 'List item')
-        .expectBraille('1. pork lstitm lst +3')
-        .call(doCmd('nextObject'))
-        .expectSpeech('2. beef', 'List item')
-        .expectBraille('2. beef lstitm')
+      // Object nav should be the same.
+      .call(doCmd('nextObject'))
+      .expectSpeech('1. pork', 'List item')
+      .expectBraille('1. pork lstitm lst +3')
+      .call(doCmd('nextObject'))
+      .expectSpeech('2. beef', 'List item')
+      .expectBraille('2. beef lstitm')
 
-        // Mixing with line nav.
-        .call(doCmd('nextLine'))
-        .expectSpeech('3. chicken', 'List item')
-        .expectBraille('3. chicken lstitm lst end')
-        .replay();
-  });
+      // Mixing with line nav.
+      .call(doCmd('nextLine'))
+      .expectSpeech('3. chicken', 'List item')
+      .expectBraille('3. chicken lstitm lst end')
+      .replay();
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'BusyHeading', function() {
+TEST_F('ChromeVoxBackgroundTest', 'BusyHeading', async function() {
   this.resetContextualOutput();
   const mockFeedback = this.createMockFeedback();
   const site = `
     <p>start</p>
     <h2><a href="#">Lots</a><a href="#">going</a><a href="#">here</a></h2>
   `;
-  this.runWithLoadedTree(site, function(root) {
-    // In the past, this would have inserted the 'heading 2' after the first
-    // link's output. Make sure it goes to the end.
-    mockFeedback.call(doCmd('nextLine'))
-        .expectSpeech(
-            'Lots', 'Link', 'going', 'Link', 'here', 'Link', 'Heading 2')
-        .expectBraille('Lots lnk going lnk here lnk h2')
-        .replay();
-  });
+  const root = await this.runWithLoadedTree(site);
+  // In the past, this would have inserted the 'heading 2' after the first
+  // link's output. Make sure it goes to the end.
+  mockFeedback.call(doCmd('nextLine'))
+      .expectSpeech(
+          'Lots', 'Link', 'going', 'Link', 'here', 'Link', 'Heading 2')
+      .expectBraille('Lots lnk going lnk here lnk h2')
+      .replay();
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'NodeVsSubnode', function() {
+TEST_F('ChromeVoxBackgroundTest', 'NodeVsSubnode', async function() {
   const mockFeedback = this.createMockFeedback();
-  this.runWithLoadedTree('<a href="#">test</a>', function(root) {
-    const link = root.find({role: RoleType.LINK});
-    function outputLinkRange(start, end) {
-      return function() {
-        new Output()
-            .withSpeech(new cursors.Range(
-                new cursors.Cursor(link, start), new cursors.Cursor(link, end)))
-            .go();
-      };
-    }
+  const root = await this.runWithLoadedTree('<a href="#">test</a>');
+  const link = root.find({role: RoleType.LINK});
+  function outputLinkRange(start, end) {
+    return function() {
+      new Output()
+          .withSpeech(new cursors.Range(
+              new cursors.Cursor(link, start), new cursors.Cursor(link, end)))
+          .go();
+    };
+  }
 
-    mockFeedback.call(outputLinkRange(0, 0))
-        .expectSpeech('test', 'Internal link')
-        .call(outputLinkRange(0, 1))
-        .expectSpeech('t')
-        .call(outputLinkRange(1, 1))
-        .expectSpeech('test', 'Internal link')
-        .call(outputLinkRange(1, 2))
-        .expectSpeech('e')
-        .call(outputLinkRange(1, 3))
-        .expectNextSpeechUtteranceIsNot('Internal link')
-        .expectSpeech('es')
-        .call(outputLinkRange(0, 4))
-        .expectSpeech('test', 'Internal link')
-        .replay();
-  });
+  mockFeedback.call(outputLinkRange(0, 0))
+      .expectSpeech('test', 'Internal link')
+      .call(outputLinkRange(0, 1))
+      .expectSpeech('t')
+      .call(outputLinkRange(1, 1))
+      .expectSpeech('test', 'Internal link')
+      .call(outputLinkRange(1, 2))
+      .expectSpeech('e')
+      .call(outputLinkRange(1, 3))
+      .expectNextSpeechUtteranceIsNot('Internal link')
+      .expectSpeech('es')
+      .call(outputLinkRange(0, 4))
+      .expectSpeech('test', 'Internal link')
+      .replay();
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'NativeFind', function() {
+TEST_F('ChromeVoxBackgroundTest', 'NativeFind', async function() {
   const mockFeedback = this.createMockFeedback();
   const site = `
     <a href="#">grape</a>
     <a href="#">pineapple</a>
   `;
-  this.runWithLoadedTree(site, function(root) {
-    mockFeedback.call(press(KeyCode.F, {ctrl: true}))
-        .expectSpeech('Find', 'Edit text')
-        .call(press(KeyCode.G))
-        .expectSpeech('grape', 'Link')
-        .call(press(KeyCode.BACK))
-        .call(press(KeyCode.L))
-        .expectSpeech('pineapple', 'Link')
-        .replay();
-  });
+  const root = await this.runWithLoadedTree(site);
+  mockFeedback.call(press(KeyCode.F, {ctrl: true}))
+      .expectSpeech('Find', 'Edit text')
+      .call(press(KeyCode.G))
+      .expectSpeech('grape', 'Link')
+      .call(press(KeyCode.BACK))
+      .call(press(KeyCode.L))
+      .expectSpeech('pineapple', 'Link')
+      .replay();
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'EditableKeyCommand', function() {
+TEST_F('ChromeVoxBackgroundTest', 'EditableKeyCommand', async function() {
   const mockFeedback = this.createMockFeedback();
   const site = `
     <input type="text"></input>
     <textarea>test</textarea>
     <div role="textbox" contenteditable>test</div>
   `;
-  this.runWithLoadedTree(site, function(root) {
-    const assertCurNode = function(node) {
-      return function() {
-        assertEquals(node, ChromeVoxState.instance.currentRange.start.node);
-      };
+  const root = await this.runWithLoadedTree(site);
+  const assertCurNode = function(node) {
+    return function() {
+      assertEquals(node, ChromeVoxState.instance.currentRange.start.node);
     };
+  };
 
-    const textField = root.firstChild;
-    const textArea = textField.nextSibling;
-    const contentEditable = textArea.nextSibling;
+  const textField = root.firstChild;
+  const textArea = textField.nextSibling;
+  const contentEditable = textArea.nextSibling;
 
-    mockFeedback.call(assertCurNode(textField))
-        .call(doCmd('nextObject'))
-        .call(assertCurNode(textArea))
-        .call(doCmd('nextObject'))
-        .call(assertCurNode(contentEditable))
-        .call(doCmd('previousObject'))
-        .expectSpeech('Text area')
-        .call(assertCurNode(textArea))
-        .call(doCmd('previousObject'))
-        .call(assertCurNode(textField))
+  mockFeedback.call(assertCurNode(textField))
+      .call(doCmd('nextObject'))
+      .call(assertCurNode(textArea))
+      .call(doCmd('nextObject'))
+      .call(assertCurNode(contentEditable))
+      .call(doCmd('previousObject'))
+      .expectSpeech('Text area')
+      .call(assertCurNode(textArea))
+      .call(doCmd('previousObject'))
+      .call(assertCurNode(textField))
 
-        .replay();
-  });
+      .replay();
 });
 
 // TODO(crbug.com/935678): Test times out flakily in MSAN builds.
@@ -1470,11 +1431,11 @@
 #define MAYBE_TextSelectionAndLiveRegion TextSelectionAndLiveRegion
 #endif
 `,
-    'ChromeVoxBackgroundTest', 'MAYBE_TextSelectionAndLiveRegion', function() {
+    'ChromeVoxBackgroundTest', 'MAYBE_TextSelectionAndLiveRegion',
+    async function() {
       BaseAutomationHandler.announceActions = true;
       const mockFeedback = this.createMockFeedback();
-      this.runWithLoadedTree(
-          `
+      const root = await this.runWithLoadedTree(`
     <p>start</p>
     <div><input value="test" type="text"></input></div>
     <div id="live" aria-live="assertive"></div>
@@ -1495,28 +1456,26 @@
         }
       });
     </script>
-  `,
-          function(root) {
-            const textField = root.find({role: RoleType.TEXT_FIELD});
-            const div = textField.parent;
-            mockFeedback.call(textField.focus.bind(textField))
-                .expectSpeech('Edit text')
-                .call(div.doDefault.bind(div))
-                .expectSpeechWithQueueMode('go', QueueMode.CATEGORY_FLUSH)
+  `);
+      const textField = root.find({role: RoleType.TEXT_FIELD});
+      const div = textField.parent;
+      mockFeedback.call(textField.focus.bind(textField))
+          .expectSpeech('Edit text')
+          .call(div.doDefault.bind(div))
+          .expectSpeechWithQueueMode('go', QueueMode.CATEGORY_FLUSH)
 
-                .call(div.doDefault.bind(div))
-                .expectSpeechWithQueueMode('queued', QueueMode.QUEUE)
-                .expectSpeechWithQueueMode('e', QueueMode.CATEGORY_FLUSH)
+          .call(div.doDefault.bind(div))
+          .expectSpeechWithQueueMode('queued', QueueMode.QUEUE)
+          .expectSpeechWithQueueMode('e', QueueMode.CATEGORY_FLUSH)
 
-                .call(div.doDefault.bind(div))
-                .expectSpeechWithQueueMode('interrupted', QueueMode.QUEUE)
-                .expectSpeechWithQueueMode('s', QueueMode.CATEGORY_FLUSH)
+          .call(div.doDefault.bind(div))
+          .expectSpeechWithQueueMode('interrupted', QueueMode.QUEUE)
+          .expectSpeechWithQueueMode('s', QueueMode.CATEGORY_FLUSH)
 
-                .replay();
-          });
+          .replay();
     });
 
-TEST_F('ChromeVoxBackgroundTest', 'TableColumnHeaders', function() {
+TEST_F('ChromeVoxBackgroundTest', 'TableColumnHeaders', async function() {
   const mockFeedback = this.createMockFeedback();
   const site = `
     <div role="grid">
@@ -1541,23 +1500,22 @@
       </div>
     </div>
   `;
-  this.runWithLoadedTree(site, function(root) {
-    mockFeedback.call(doCmd('nextRow'))
-        .expectSpeech('Mountain View', 'row 2 column 1')
-        .call(doCmd('nextRow'))
-        .expectNextSpeechUtteranceIsNot('city')
-        .expectSpeech('San Jose', 'row 3 column 1')
-        .call(doCmd('nextCol'))
-        .expectSpeech('CA', 'row 3 column 2', 'state')
-        .call(doCmd('previousRow'))
-        .expectSpeech('CA', 'row 2 column 2')
-        .call(doCmd('previousRow'))
-        .expectSpeech('state', 'row 1 column 2')
-        .replay();
-  });
+  const root = await this.runWithLoadedTree(site);
+  mockFeedback.call(doCmd('nextRow'))
+      .expectSpeech('Mountain View', 'row 2 column 1')
+      .call(doCmd('nextRow'))
+      .expectNextSpeechUtteranceIsNot('city')
+      .expectSpeech('San Jose', 'row 3 column 1')
+      .call(doCmd('nextCol'))
+      .expectSpeech('CA', 'row 3 column 2', 'state')
+      .call(doCmd('previousRow'))
+      .expectSpeech('CA', 'row 2 column 2')
+      .call(doCmd('previousRow'))
+      .expectSpeech('state', 'row 1 column 2')
+      .replay();
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'ActiveDescendantUpdates', function() {
+TEST_F('ChromeVoxBackgroundTest', 'ActiveDescendantUpdates', async function() {
   const mockFeedback = this.createMockFeedback();
   const site = `
     <div aria-label="container" tabindex=0 role="group" id="active"
@@ -1576,18 +1534,17 @@
       });
       </script>
   `;
-  this.runWithLoadedTree(site, function(root) {
-    const group = root.firstChild;
-    mockFeedback.call(group.focus.bind(group))
-        .call(group.doDefault.bind(group))
-        .expectSpeech('Tree item', ' 2 of 2 ')
-        .call(group.doDefault.bind(group))
-        .expectSpeech('Tree item', 'Not selected', ' 1 of 2 ')
-        .replay();
-  });
+  const root = await this.runWithLoadedTree(site);
+  const group = root.firstChild;
+  mockFeedback.call(group.focus.bind(group))
+      .call(group.doDefault.bind(group))
+      .expectSpeech('Tree item', ' 2 of 2 ')
+      .call(group.doDefault.bind(group))
+      .expectSpeech('Tree item', 'Not selected', ' 1 of 2 ')
+      .replay();
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'NavigationEscapesEdit', function() {
+TEST_F('ChromeVoxBackgroundTest', 'NavigationEscapesEdit', async function() {
   const mockFeedback = this.createMockFeedback();
   const site = `
     <p>before content editable</p>
@@ -1596,100 +1553,96 @@
     <textarea style="word-spacing: 1000px">this is a test</textarea>
     <p>after text area</p>
   `;
-  this.runWithLoadedTree(site, function(root) {
-    const assertBeginning = function(expected) {
-      const textEditHandler =
-          DesktopAutomationInterface.instance.textEditHandler;
-      assertNotNullNorUndefined(textEditHandler);
-      assertEquals(expected, textEditHandler.isSelectionOnFirstLine());
-    };
-    const assertEnd = function(expected) {
-      const textEditHandler =
-          DesktopAutomationInterface.instance.textEditHandler;
-      assertNotNullNorUndefined(textEditHandler);
-      assertEquals(expected, textEditHandler.isSelectionOnLastLine());
-    };
-    const [contentEditable, textArea] =
-        root.findAll({role: RoleType.TEXT_FIELD});
+  const root = await this.runWithLoadedTree(site);
+  const assertBeginning = function(expected) {
+    const textEditHandler = DesktopAutomationInterface.instance.textEditHandler;
+    assertNotNullNorUndefined(textEditHandler);
+    assertEquals(expected, textEditHandler.isSelectionOnFirstLine());
+  };
+  const assertEnd = function(expected) {
+    const textEditHandler = DesktopAutomationInterface.instance.textEditHandler;
+    assertNotNullNorUndefined(textEditHandler);
+    assertEquals(expected, textEditHandler.isSelectionOnLastLine());
+  };
+  const [contentEditable, textArea] = root.findAll({role: RoleType.TEXT_FIELD});
 
-    this.listenOnce(contentEditable, EventType.FOCUS, function() {
-      mockFeedback.call(assertBeginning.bind(this, true))
-          .call(assertEnd.bind(this, false))
+  this.listenOnce(contentEditable, EventType.FOCUS, function() {
+    mockFeedback.call(assertBeginning.bind(this, true))
+        .call(assertEnd.bind(this, false))
 
-          .call(press(KeyCode.DOWN))
-          .expectSpeech('is')
-          .call(assertBeginning.bind(this, false))
-          .call(assertEnd.bind(this, false))
+        .call(press(KeyCode.DOWN))
+        .expectSpeech('is')
+        .call(assertBeginning.bind(this, false))
+        .call(assertEnd.bind(this, false))
 
-          .call(press(KeyCode.DOWN))
-          .expectSpeech('a')
-          .call(assertBeginning.bind(this, false))
-          .call(assertEnd.bind(this, false))
+        .call(press(KeyCode.DOWN))
+        .expectSpeech('a')
+        .call(assertBeginning.bind(this, false))
+        .call(assertEnd.bind(this, false))
 
-          .call(press(KeyCode.DOWN))
-          .expectSpeech('test')
-          .call(assertBeginning.bind(this, false))
-          .call(assertEnd.bind(this, true))
+        .call(press(KeyCode.DOWN))
+        .expectSpeech('test')
+        .call(assertBeginning.bind(this, false))
+        .call(assertEnd.bind(this, true))
 
-          .call(textArea.focus.bind(textArea))
-          .expectSpeech('Text area')
-          .call(assertBeginning.bind(this, true))
-          .call(assertEnd.bind(this, false))
+        .call(textArea.focus.bind(textArea))
+        .expectSpeech('Text area')
+        .call(assertBeginning.bind(this, true))
+        .call(assertEnd.bind(this, false))
 
-          .call(press(40 /* ArrowDown */))
-          .expectSpeech('is')
-          .call(assertBeginning.bind(this, false))
-          .call(assertEnd.bind(this, false))
+        .call(press(40 /* ArrowDown */))
+        .expectSpeech('is')
+        .call(assertBeginning.bind(this, false))
+        .call(assertEnd.bind(this, false))
 
-          .call(press(40 /* ArrowDown */))
-          .expectSpeech('a')
-          .call(assertBeginning.bind(this, false))
-          .call(assertEnd.bind(this, false))
+        .call(press(40 /* ArrowDown */))
+        .expectSpeech('a')
+        .call(assertBeginning.bind(this, false))
+        .call(assertEnd.bind(this, false))
 
-          .call(press(40 /* ArrowDown */))
-          .expectSpeech('test')
-          .call(assertBeginning.bind(this, false))
-          .call(assertEnd.bind(this, true))
+        .call(press(40 /* ArrowDown */))
+        .expectSpeech('test')
+        .call(assertBeginning.bind(this, false))
+        .call(assertEnd.bind(this, true))
 
-          .replay();
+        .replay();
 
-      // TODO: soft line breaks currently won't work in <textarea>.
-    }.bind(this));
-    contentEditable.focus();
-  });
+    // TODO: soft line breaks currently won't work in <textarea>.
+  }.bind(this));
+  contentEditable.focus();
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'SelectDoesNotSyncNavigation', function() {
-  const mockFeedback = this.createMockFeedback();
-  const site = `
+TEST_F(
+    'ChromeVoxBackgroundTest', 'SelectDoesNotSyncNavigation', async function() {
+      const mockFeedback = this.createMockFeedback();
+      const site = `
     <select>
       <option>apple</option>
       <option>grape</option>
     </select>
   `;
-  this.runWithLoadedTree(site, function(root) {
-    const select = root.find({role: RoleType.POP_UP_BUTTON});
-    mockFeedback.expectSpeech('Button', 'has pop up', 'Collapsed')
-        .call(select.doDefault.bind(select))
-        .expectSpeech('Expanded')
-        .call(
-            () => assertEquals(
-                select, ChromeVoxState.instance.currentRange.start.node))
-        .call(press(KeyCode.DOWN))
-        .expectSpeech('grape', 'List item', ' 2 of 2 ')
-        .call(
-            () => assertEquals(
-                select, ChromeVoxState.instance.currentRange.start.node))
-        .call(press(KeyCode.UP))
-        .expectSpeech('apple', 'List item', ' 1 of 2 ')
-        .call(
-            () => assertEquals(
-                select, ChromeVoxState.instance.currentRange.start.node))
-        .replay();
-  });
-});
+      const root = await this.runWithLoadedTree(site);
+      const select = root.find({role: RoleType.POP_UP_BUTTON});
+      mockFeedback.expectSpeech('Button', 'has pop up', 'Collapsed')
+          .call(select.doDefault.bind(select))
+          .expectSpeech('Expanded')
+          .call(
+              () => assertEquals(
+                  select, ChromeVoxState.instance.currentRange.start.node))
+          .call(press(KeyCode.DOWN))
+          .expectSpeech('grape', 'List item', ' 2 of 2 ')
+          .call(
+              () => assertEquals(
+                  select, ChromeVoxState.instance.currentRange.start.node))
+          .call(press(KeyCode.UP))
+          .expectSpeech('apple', 'List item', ' 1 of 2 ')
+          .call(
+              () => assertEquals(
+                  select, ChromeVoxState.instance.currentRange.start.node))
+          .replay();
+    });
 
-TEST_F('ChromeVoxBackgroundTest', 'NavigationIgnoresLabels', function() {
+TEST_F('ChromeVoxBackgroundTest', 'NavigationIgnoresLabels', async function() {
   const mockFeedback = this.createMockFeedback();
   const site = `
     <p>before</p>
@@ -1699,70 +1652,70 @@
     <p>after</p>
     <button aria-labelledby="label headingLabel"></button>
   `;
-  this.runWithLoadedTree(site, function(root) {
-    mockFeedback.expectSpeech('before')
-        .call(doCmd('nextObject'))
-        .expectSpeech('lebal', 'Link')
-        .call(doCmd('nextObject'))
-        .expectSpeech('headingLabel', 'Heading 2')
-        .call(doCmd('nextObject'))
-        .expectSpeech('after')
-        .call(doCmd('previousObject'))
-        .expectSpeech('headingLabel', 'Heading 2')
-        .call(doCmd('previousObject'))
-        .expectSpeech('lebal', 'Link')
-        .call(doCmd('previousObject'))
-        .expectSpeech('before')
-        .call(doCmd('nextObject'))
-        .expectSpeech('lebal', 'Link')
-        .call(doCmd('nextObject'))
-        .expectSpeech('headingLabel', 'Heading 2')
-        .call(doCmd('nextObject'))
-        .expectSpeech('after')
-        .call(doCmd('nextObject'))
-        .expectSpeech('label headingLabel', 'Button')
-        .call(doCmd('nextObject'))
-        .expectEarcon(Earcon.WRAP)
-        .call(doCmd('nextObject'))
-        .expectSpeech('before')
-        .replay();
-  });
+  const root = await this.runWithLoadedTree(site);
+  mockFeedback.expectSpeech('before')
+      .call(doCmd('nextObject'))
+      .expectSpeech('lebal', 'Link')
+      .call(doCmd('nextObject'))
+      .expectSpeech('headingLabel', 'Heading 2')
+      .call(doCmd('nextObject'))
+      .expectSpeech('after')
+      .call(doCmd('previousObject'))
+      .expectSpeech('headingLabel', 'Heading 2')
+      .call(doCmd('previousObject'))
+      .expectSpeech('lebal', 'Link')
+      .call(doCmd('previousObject'))
+      .expectSpeech('before')
+      .call(doCmd('nextObject'))
+      .expectSpeech('lebal', 'Link')
+      .call(doCmd('nextObject'))
+      .expectSpeech('headingLabel', 'Heading 2')
+      .call(doCmd('nextObject'))
+      .expectSpeech('after')
+      .call(doCmd('nextObject'))
+      .expectSpeech('label headingLabel', 'Button')
+      .call(doCmd('nextObject'))
+      .expectEarcon(Earcon.WRAP)
+      .call(doCmd('nextObject'))
+      .expectSpeech('before')
+      .replay();
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'NavigationIgnoresDescriptions', function() {
-  const mockFeedback = this.createMockFeedback();
-  const site = `
+TEST_F(
+    'ChromeVoxBackgroundTest', 'NavigationIgnoresDescriptions',
+    async function() {
+      const mockFeedback = this.createMockFeedback();
+      const site = `
     <p>before</p>
     <p id="desc">label</p>
     <a href="#next" id="csed">lebal</a>
     <p>after</p>
     <button aria-describedby="desc"></button>
   `;
-  this.runWithLoadedTree(site, function(root) {
-    mockFeedback.expectSpeech('before')
-        .call(doCmd('nextObject'))
-        .expectSpeech('lebal', 'Link')
-        .call(doCmd('nextObject'))
-        .expectSpeech('after')
-        .call(doCmd('previousObject'))
-        .expectSpeech('lebal', 'Link')
-        .call(doCmd('previousObject'))
-        .expectSpeech('before')
-        .call(doCmd('nextObject'))
-        .expectSpeech('lebal', 'Link')
-        .call(doCmd('nextObject'))
-        .expectSpeech('after')
-        .call(doCmd('nextObject'))
-        .expectSpeech('label', 'lebal', 'Button')
-        .call(doCmd('nextObject'))
-        .expectEarcon(Earcon.WRAP)
-        .call(doCmd('nextObject'))
-        .expectSpeech('before')
-        .replay();
-  });
-});
+      const root = await this.runWithLoadedTree(site);
+      mockFeedback.expectSpeech('before')
+          .call(doCmd('nextObject'))
+          .expectSpeech('lebal', 'Link')
+          .call(doCmd('nextObject'))
+          .expectSpeech('after')
+          .call(doCmd('previousObject'))
+          .expectSpeech('lebal', 'Link')
+          .call(doCmd('previousObject'))
+          .expectSpeech('before')
+          .call(doCmd('nextObject'))
+          .expectSpeech('lebal', 'Link')
+          .call(doCmd('nextObject'))
+          .expectSpeech('after')
+          .call(doCmd('nextObject'))
+          .expectSpeech('label', 'lebal', 'Button')
+          .call(doCmd('nextObject'))
+          .expectEarcon(Earcon.WRAP)
+          .call(doCmd('nextObject'))
+          .expectSpeech('before')
+          .replay();
+    });
 
-TEST_F('ChromeVoxBackgroundTest', 'MathContentViaInnerHtml', function() {
+TEST_F('ChromeVoxBackgroundTest', 'MathContentViaInnerHtml', async function() {
   const mockFeedback = this.createMockFeedback();
   const site = `
     <div role="math">
@@ -1797,15 +1750,14 @@
       </semantics>
     </div>
   `;
-  this.runWithLoadedTree(site, function(root) {
-    mockFeedback.call(doCmd('nextObject'))
-        .expectSpeech('a ( y + m ) squared + b ( y + m ) + c = 0 .')
-        .expectSpeech('Press up, down, left, or right to explore math')
-        .replay();
-  });
+  const root = await this.runWithLoadedTree(site);
+  mockFeedback.call(doCmd('nextObject'))
+      .expectSpeech('a ( y + m ) squared + b ( y + m ) + c = 0 .')
+      .expectSpeech('Press up, down, left, or right to explore math')
+      .replay();
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'GestureGranularity', function() {
+TEST_F('ChromeVoxBackgroundTest', 'GestureGranularity', async function() {
   const mockFeedback = this.createMockFeedback();
   const site = `
     <p>This is a test</p>
@@ -1816,65 +1768,64 @@
     <a href="#">there</a>
     <button>world</button>
   `;
-  this.runWithLoadedTree(site, function(root) {
-    mockFeedback.call(doGesture(Gesture.SWIPE_LEFT3))
-        .expectSpeech('Word')
-        .call(doGesture(Gesture.SWIPE_DOWN1))
-        .expectSpeech('is')
-        .call(doGesture(Gesture.SWIPE_DOWN1))
-        .expectSpeech('a')
-        .call(doGesture(Gesture.SWIPE_UP1))
-        .expectSpeech('is')
+  const root = await this.runWithLoadedTree(site);
+  mockFeedback.call(doGesture(Gesture.SWIPE_LEFT3))
+      .expectSpeech('Word')
+      .call(doGesture(Gesture.SWIPE_DOWN1))
+      .expectSpeech('is')
+      .call(doGesture(Gesture.SWIPE_DOWN1))
+      .expectSpeech('a')
+      .call(doGesture(Gesture.SWIPE_UP1))
+      .expectSpeech('is')
 
-        .call(doGesture(Gesture.SWIPE_LEFT3))
-        .expectSpeech('Character')
-        .call(doGesture(Gesture.SWIPE_DOWN1))
-        .expectSpeech('s')
-        .call(doGesture(Gesture.SWIPE_UP1))
-        .expectSpeech('i')
+      .call(doGesture(Gesture.SWIPE_LEFT3))
+      .expectSpeech('Character')
+      .call(doGesture(Gesture.SWIPE_DOWN1))
+      .expectSpeech('s')
+      .call(doGesture(Gesture.SWIPE_UP1))
+      .expectSpeech('i')
 
-        .call(doGesture(Gesture.SWIPE_LEFT3))
-        .expectSpeech('Form field control')
-        .call(doGesture(Gesture.SWIPE_DOWN1))
-        .expectSpeech('and', 'Button')
-        .call(doGesture(Gesture.SWIPE_UP1))
-        .expectSpeech('world', 'Button')
+      .call(doGesture(Gesture.SWIPE_LEFT3))
+      .expectSpeech('Form field control')
+      .call(doGesture(Gesture.SWIPE_DOWN1))
+      .expectSpeech('and', 'Button')
+      .call(doGesture(Gesture.SWIPE_UP1))
+      .expectSpeech('world', 'Button')
 
-        .call(doGesture(Gesture.SWIPE_LEFT3))
-        .expectSpeech('Link')
-        .call(doGesture(Gesture.SWIPE_DOWN1))
-        .expectSpeech('greetings', 'Internal link')
-        .call(doGesture(Gesture.SWIPE_UP1))
-        .expectSpeech('there', 'Internal link')
+      .call(doGesture(Gesture.SWIPE_LEFT3))
+      .expectSpeech('Link')
+      .call(doGesture(Gesture.SWIPE_DOWN1))
+      .expectSpeech('greetings', 'Internal link')
+      .call(doGesture(Gesture.SWIPE_UP1))
+      .expectSpeech('there', 'Internal link')
 
-        .call(doGesture(Gesture.SWIPE_LEFT3))
-        .expectSpeech('Heading')
-        .call(doGesture(Gesture.SWIPE_DOWN1))
-        .expectSpeech('hello', 'Heading 2')
-        .call(doGesture(Gesture.SWIPE_UP1))
-        .expectSpeech('here', 'Heading 2')
-        .call(doGesture(Gesture.SWIPE_UP1))
-        .expectSpeech('hello', 'Heading 2')
+      .call(doGesture(Gesture.SWIPE_LEFT3))
+      .expectSpeech('Heading')
+      .call(doGesture(Gesture.SWIPE_DOWN1))
+      .expectSpeech('hello', 'Heading 2')
+      .call(doGesture(Gesture.SWIPE_UP1))
+      .expectSpeech('here', 'Heading 2')
+      .call(doGesture(Gesture.SWIPE_UP1))
+      .expectSpeech('hello', 'Heading 2')
 
-        .call(doGesture(Gesture.SWIPE_LEFT3))
-        .expectSpeech('Line')
-        .call(doGesture(Gesture.SWIPE_UP1))
-        .expectSpeech('This is a test')
+      .call(doGesture(Gesture.SWIPE_LEFT3))
+      .expectSpeech('Line')
+      .call(doGesture(Gesture.SWIPE_UP1))
+      .expectSpeech('This is a test')
 
-        .call(doGesture(Gesture.SWIPE_RIGHT3))
-        .expectSpeech('Heading')
-        .call(doGesture(Gesture.SWIPE_RIGHT3))
-        .expectSpeech('Internal link')
-        .call(doGesture(Gesture.SWIPE_RIGHT3))
-        .expectSpeech('Form field control')
-        .call(doGesture(Gesture.SWIPE_RIGHT3))
-        .expectSpeech('Character')
+      .call(doGesture(Gesture.SWIPE_RIGHT3))
+      .expectSpeech('Heading')
+      .call(doGesture(Gesture.SWIPE_RIGHT3))
+      .expectSpeech('Internal link')
+      .call(doGesture(Gesture.SWIPE_RIGHT3))
+      .expectSpeech('Form field control')
+      .call(doGesture(Gesture.SWIPE_RIGHT3))
+      .expectSpeech('Character')
 
-        .replay();
-  });
+      .replay();
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'LinesFilterWhitespace', function() {
+TEST_F('ChromeVoxBackgroundTest', 'LinesFilterWhitespace', async function() {
   const mockFeedback = this.createMockFeedback();
   const site = `
     <p>start</p>
@@ -1885,21 +1836,21 @@
       </div>
     </div>
   `;
-  this.runWithLoadedTree(site, function(root) {
-    mockFeedback.expectSpeech('start')
-        .clearPendingOutput()
-        .call(doCmd('nextLine'))
-        .expectSpeech('Munich')
-        .expectNextSpeechUtteranceIsNot(' ')
-        .expectSpeech('London')
-        .replay();
-  });
+  const root = await this.runWithLoadedTree(site);
+  mockFeedback.expectSpeech('start')
+      .clearPendingOutput()
+      .call(doCmd('nextLine'))
+      .expectSpeech('Munich')
+      .expectNextSpeechUtteranceIsNot(' ')
+      .expectSpeech('London')
+      .replay();
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'TabSwitchAndRefreshRecovery', function() {
-  const mockFeedback = this.createMockFeedback();
-  this.runWithLoadedTree('<p>tab1</p>', function(root1) {
-    this.runWithLoadedTree('<p>tab2</p>', function(root2) {
+TEST_F(
+    'ChromeVoxBackgroundTest', 'TabSwitchAndRefreshRecovery', async function() {
+      const mockFeedback = this.createMockFeedback();
+      const root1 = await this.runWithLoadedTree('<p>tab1</p>');
+      const root2 = await this.runWithLoadedTree('<p>tab2</p>');
       mockFeedback.expectSpeech('tab2')
           .clearPendingOutput()
           .call(press(KeyCode.TAB, {shift: true, ctrl: true}))
@@ -1917,10 +1868,8 @@
           })
           .replay();
     });
-  });
-});
 
-TEST_F('ChromeVoxBackgroundTest', 'ListName', function() {
+TEST_F('ChromeVoxBackgroundTest', 'ListName', async function() {
   const mockFeedback = this.createMockFeedback();
   const site = `
     <div id="_md-chips-wrapper-76" tabindex="-1" class="md-chips md-readonly"
@@ -1932,27 +1881,25 @@
       <div role="listitem">Football</div>
     </div>
   `;
-  this.runWithLoadedTree(site, function(root) {
-    mockFeedback.expectSpeech('Favorite Sports').replay();
-  });
+  const root = await this.runWithLoadedTree(site);
+  mockFeedback.expectSpeech('Favorite Sports').replay();
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'LayoutTable', function() {
+TEST_F('ChromeVoxBackgroundTest', 'LayoutTable', async function() {
   const mockFeedback = this.createMockFeedback();
   const site = `
     <table><tr><td>start</td></tr></table><p>end</p>
   `;
-  this.runWithLoadedTree(site, function(root) {
-    mockFeedback.expectSpeech('start')
-        .call(doCmd('nextObject'))
-        .expectNextSpeechUtteranceIsNot('row 1 column 1')
-        .expectNextSpeechUtteranceIsNot('Table , 1 by 1')
-        .expectSpeech('end')
-        .replay();
-  });
+  const root = await this.runWithLoadedTree(site);
+  mockFeedback.expectSpeech('start')
+      .call(doCmd('nextObject'))
+      .expectNextSpeechUtteranceIsNot('row 1 column 1')
+      .expectNextSpeechUtteranceIsNot('Table , 1 by 1')
+      .expectSpeech('end')
+      .replay();
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'ReinsertedNodeRecovery', function() {
+TEST_F('ChromeVoxBackgroundTest', 'ReinsertedNodeRecovery', async function() {
   const mockFeedback = this.createMockFeedback();
   const site = `
     <div>
@@ -1970,39 +1917,37 @@
       });
     </script>
   `;
-  this.runWithLoadedTree(site, function(root) {
-    mockFeedback.expectSpeech('start')
-        .clearPendingOutput()
-        .call(doCmd('nextObject'))
-        .call(doCmd('nextObject'))
-        .call(doCmd('nextObject'))
-        .expectSpeech('end', 'Button')
-        .replay();
-  });
+  const root = await this.runWithLoadedTree(site);
+  mockFeedback.expectSpeech('start')
+      .clearPendingOutput()
+      .call(doCmd('nextObject'))
+      .call(doCmd('nextObject'))
+      .call(doCmd('nextObject'))
+      .expectSpeech('end', 'Button')
+      .replay();
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'PointerTargetsLeafNode', function() {
+TEST_F('ChromeVoxBackgroundTest', 'PointerTargetsLeafNode', async function() {
   const mockFeedback = this.createMockFeedback();
   const site = `
     <div role=button><p>Washington</p></div>
     <div role=button><p>Adams</p></div>
     <div role=button><p>Jefferson</p></div>
   `;
-  this.runWithLoadedTree(site, function(root) {
-    const button =
-        root.find({role: RoleType.BUTTON, attributes: {name: 'Jefferson'}});
-    const buttonP = button.firstChild;
-    assertNotNullNorUndefined(buttonP);
-    const buttonText = buttonP.firstChild;
-    assertNotNullNorUndefined(buttonText);
-    mockFeedback.call(simulateHitTestResult(buttonText))
-        .expectSpeech('Jefferson')
-        .expectSpeech('Button')
-        .replay();
-  });
+  const root = await this.runWithLoadedTree(site);
+  const button =
+      root.find({role: RoleType.BUTTON, attributes: {name: 'Jefferson'}});
+  const buttonP = button.firstChild;
+  assertNotNullNorUndefined(buttonP);
+  const buttonText = buttonP.firstChild;
+  assertNotNullNorUndefined(buttonText);
+  mockFeedback.call(simulateHitTestResult(buttonText))
+      .expectSpeech('Jefferson')
+      .expectSpeech('Button')
+      .replay();
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'AriaSliderWithValueNow', function() {
+TEST_F('ChromeVoxBackgroundTest', 'AriaSliderWithValueNow', async function() {
   const mockFeedback = this.createMockFeedback();
   const site = `
     <div id="slider" role="slider" tabindex="0" aria-valuemin="0"
@@ -2015,16 +1960,13 @@
       });
     </script>
   `;
-  this.runWithLoadedTree(site, function(root) {
-    const slider = root.find({role: RoleType.SLIDER});
-    assertNotNullNorUndefined(slider);
-    mockFeedback.call(slider.doDefault.bind(slider))
-        .expectSpeech('51')
-        .replay();
-  });
+  const root = await this.runWithLoadedTree(site);
+  const slider = root.find({role: RoleType.SLIDER});
+  assertNotNullNorUndefined(slider);
+  mockFeedback.call(slider.doDefault.bind(slider)).expectSpeech('51').replay();
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'AriaSliderWithValueText', function() {
+TEST_F('ChromeVoxBackgroundTest', 'AriaSliderWithValueText', async function() {
   const mockFeedback = this.createMockFeedback();
   const site = `
     <div id="slider" role="slider" tabindex="0" aria-valuemin="0"
@@ -2038,18 +1980,17 @@
       });
     </script>
   `;
-  this.runWithLoadedTree(site, function(root) {
-    const slider = root.find({role: RoleType.SLIDER});
-    assertNotNullNorUndefined(slider);
-    mockFeedback.clearPendingOutput()
-        .call(slider.doDefault.bind(slider))
-        .expectNextSpeechUtteranceIsNot('51')
-        .expectSpeech('large')
-        .replay();
-  });
+  const root = await this.runWithLoadedTree(site);
+  const slider = root.find({role: RoleType.SLIDER});
+  assertNotNullNorUndefined(slider);
+  mockFeedback.clearPendingOutput()
+      .call(slider.doDefault.bind(slider))
+      .expectNextSpeechUtteranceIsNot('51')
+      .expectSpeech('large')
+      .replay();
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'SelectValidityOutput', function() {
+TEST_F('ChromeVoxBackgroundTest', 'SelectValidityOutput', async function() {
   const mockFeedback = this.createMockFeedback();
   const site = `
     <p>start</p>
@@ -2065,92 +2006,89 @@
       });
     </script>
   `;
-  this.runWithLoadedTree(site, function(root) {
-    mockFeedback.expectSpeech('start')
-        .call(doCmd('nextObject'))
-        .expectSpeech('Name:')
-        .expectSpeech('Edit text')
-        .expectSpeech('Required')
-        .expectNextSpeechUtteranceIsNot('Alert')
-        .expectSpeech('Please enter name')
-        .replay();
-  });
+  const root = await this.runWithLoadedTree(site);
+  mockFeedback.expectSpeech('start')
+      .call(doCmd('nextObject'))
+      .expectSpeech('Name:')
+      .expectSpeech('Edit text')
+      .expectSpeech('Required')
+      .expectNextSpeechUtteranceIsNot('Alert')
+      .expectSpeech('Please enter name')
+      .replay();
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'EventFromAction', function() {
+TEST_F('ChromeVoxBackgroundTest', 'EventFromAction', async function() {
   const site = '<button>ok</button><button>cancel</button>';
-  this.runWithLoadedTree(site, function(root) {
-    const button = root.findAll({role: RoleType.BUTTON})[1];
-    button.addEventListener(EventType.FOCUS, this.newCallback(function(evt) {
-      assertEquals(RoleType.BUTTON, evt.target.role);
-      assertEquals('action', evt.eventFrom);
-      assertEquals('cancel', evt.target.name);
-      assertEquals('focus', evt.eventFromAction);
-    }));
-
-    button.focus();
-  });
-});
-
-TEST_F('ChromeVoxBackgroundTest', 'EventFromUser', function() {
-  const site = '<button>ok</button><button>cancel</button>';
-  this.runWithLoadedTree(site, async function(root) {
-    const buttons = root.findAll({role: RoleType.BUTTON});
-    const okButton = buttons[0];
-    const cancelButton = buttons[1];
-
-    await new Promise(r => {
-      if (okButton.state.focused) {
-        r();
-      } else {
-        okButton.addEventListener('focus', r);
-      }
-    });
-
-    press(KeyCode.TAB)();
-
-    const evt =
-        await new Promise(r => cancelButton.addEventListener('focus', r));
+  const root = await this.runWithLoadedTree(site);
+  const button = root.findAll({role: RoleType.BUTTON})[1];
+  button.addEventListener(EventType.FOCUS, this.newCallback(function(evt) {
     assertEquals(RoleType.BUTTON, evt.target.role);
-    assertEquals('user', evt.eventFrom);
+    assertEquals('action', evt.eventFrom);
     assertEquals('cancel', evt.target.name);
-  });
+    assertEquals('focus', evt.eventFromAction);
+  }));
+
+  button.focus();
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'ReadPhoneticPronunciationTest', function() {
-  const mockFeedback = this.createMockFeedback();
-  const site = `
+TEST_F('ChromeVoxBackgroundTest', 'EventFromUser', async function() {
+  const site = '<button>ok</button><button>cancel</button>';
+  const root = await this.runWithLoadedTree(site);
+  const buttons = root.findAll({role: RoleType.BUTTON});
+  const okButton = buttons[0];
+  const cancelButton = buttons[1];
+
+  await new Promise(r => {
+    if (okButton.state.focused) {
+      r();
+    } else {
+      okButton.addEventListener('focus', r);
+    }
+  });
+
+  press(KeyCode.TAB)();
+
+  const evt = await new Promise(r => cancelButton.addEventListener('focus', r));
+  assertEquals(RoleType.BUTTON, evt.target.role);
+  assertEquals('user', evt.eventFrom);
+  assertEquals('cancel', evt.target.name);
+});
+
+TEST_F(
+    'ChromeVoxBackgroundTest', 'ReadPhoneticPronunciationTest',
+    async function() {
+      const mockFeedback = this.createMockFeedback();
+      const site = `
    <button>This is a button</button>
    <input type="text"></input>
   `;
-  this.runWithLoadedTree(site, function(root) {
-    root.find({role: RoleType.BUTTON}).focus();
-    mockFeedback.call(doCmd('readPhoneticPronunciation'))
-        .expectSpeech(
-            'T: tango, h: hotel, i: india, s: sierra,  : , i: india, ' +
-            's: sierra,  : , a: alpha,  : , b: bravo, u: uniform, t: tango, ' +
-            't: tango, o: oscar, n: november')
-        .call(doCmd('nextWord'))
-        .call(doCmd('readPhoneticPronunciation'))
-        .expectSpeech('i: india, s: sierra')
-        .call(doCmd('previousWord'))
-        .call(doCmd('readPhoneticPronunciation'))
-        .expectSpeech('T: tango, h: hotel, i: india, s: sierra')
-        .call(doCmd('nextWord'))
-        .call(doCmd('nextWord'))
-        .call(doCmd('nextWord'))
-        .call(doCmd('readPhoneticPronunciation'))
-        .expectSpeech(
-            'b: bravo, u: uniform, t: tango, t: tango, o: oscar, ' +
-            'n: november')
-        .call(doCmd('nextEditText'))
-        .call(doCmd('readPhoneticPronunciation'))
-        .expectSpeech('No available text for this item');
-    mockFeedback.replay();
-  });
-});
+      const root = await this.runWithLoadedTree(site);
+      root.find({role: RoleType.BUTTON}).focus();
+      mockFeedback.call(doCmd('readPhoneticPronunciation'))
+          .expectSpeech(
+              'T: tango, h: hotel, i: india, s: sierra,  : , i: india, ' +
+              's: sierra,  : , a: alpha,  : , b: bravo, u: uniform, t: tango, ' +
+              't: tango, o: oscar, n: november')
+          .call(doCmd('nextWord'))
+          .call(doCmd('readPhoneticPronunciation'))
+          .expectSpeech('i: india, s: sierra')
+          .call(doCmd('previousWord'))
+          .call(doCmd('readPhoneticPronunciation'))
+          .expectSpeech('T: tango, h: hotel, i: india, s: sierra')
+          .call(doCmd('nextWord'))
+          .call(doCmd('nextWord'))
+          .call(doCmd('nextWord'))
+          .call(doCmd('readPhoneticPronunciation'))
+          .expectSpeech(
+              'b: bravo, u: uniform, t: tango, t: tango, o: oscar, ' +
+              'n: november')
+          .call(doCmd('nextEditText'))
+          .call(doCmd('readPhoneticPronunciation'))
+          .expectSpeech('No available text for this item');
+      mockFeedback.replay();
+    });
 
-TEST_F('ChromeVoxBackgroundTest', 'SimilarItemNavigation', function() {
+TEST_F('ChromeVoxBackgroundTest', 'SimilarItemNavigation', async function() {
   const mockFeedback = this.createMockFeedback();
   const site = `
     <h3><a href="#a">inner</a></h3>
@@ -2159,27 +2097,26 @@
     <a href="#b">outer1</a>
     <h3>outer2</h3>
   `;
-  this.runWithLoadedTree(site, function(root) {
-    assertEquals(
-        RoleType.LINK, ChromeVoxState.instance.currentRange.start.node.role);
-    assertEquals('inner', ChromeVoxState.instance.currentRange.start.node.name);
-    mockFeedback.call(doCmd('nextSimilarItem'))
-        .expectSpeech('outer1', 'Link')
-        .call(doCmd('nextSimilarItem'))
-        .expectSpeech('inner', 'Link')
-        .call(doCmd('nextSimilarItem'))
-        .call(doCmd('previousSimilarItem'))
-        .expectSpeech('inner', 'Link')
-        .call(doCmd('nextHeading'))
-        .expectSpeech('outer2', 'Heading 3')
-        .call(doCmd('previousSimilarItem'))
-        .expectSpeech('inner', 'Heading 3');
+  const root = await this.runWithLoadedTree(site);
+  assertEquals(
+      RoleType.LINK, ChromeVoxState.instance.currentRange.start.node.role);
+  assertEquals('inner', ChromeVoxState.instance.currentRange.start.node.name);
+  mockFeedback.call(doCmd('nextSimilarItem'))
+      .expectSpeech('outer1', 'Link')
+      .call(doCmd('nextSimilarItem'))
+      .expectSpeech('inner', 'Link')
+      .call(doCmd('nextSimilarItem'))
+      .call(doCmd('previousSimilarItem'))
+      .expectSpeech('inner', 'Link')
+      .call(doCmd('nextHeading'))
+      .expectSpeech('outer2', 'Heading 3')
+      .call(doCmd('previousSimilarItem'))
+      .expectSpeech('inner', 'Heading 3');
 
-    mockFeedback.replay();
-  });
+  mockFeedback.replay();
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'InvalidItemNavigation', function() {
+TEST_F('ChromeVoxBackgroundTest', 'InvalidItemNavigation', async function() {
   const mockFeedback = this.createMockFeedback();
   const site = `
     <h3><a href="#a">inner</a></h3>
@@ -2194,54 +2131,54 @@
     <h3>outer2</h3>
   `;
 
-  this.runWithLoadedTree(site, function(root) {
-    assertEquals(
-        RoleType.LINK, ChromeVoxState.instance.currentRange.start.node.role);
-    assertEquals('inner', ChromeVoxState.instance.currentRange.start.node.name);
-    mockFeedback.call(doCmd('nextInvalidItem'))
-        .expectSpeech('txet', 'misspelled')
-        .call(doCmd('nextInvalidItem'))
-        .expectSpeech('some other reason')
-        .call(doCmd('nextInvalidItem'))
-        .expectSpeech('this are', 'grammar error')
-        .call(doCmd('nextInvalidItem'))
-        .expectSpeech('error is this')
-        // Ensure wrap.
-        .call(doCmd('nextInvalidItem'))
-        .expectSpeech('txet')
-        // Wrap backward.
-        .call(doCmd('previousInvalidItem'))
-        .expectSpeech('error is this')
-        .call(doCmd('previousInvalidItem'))
-        .expectSpeech('this are', 'grammar error');
+  const root = await this.runWithLoadedTree(site);
+  assertEquals(
+      RoleType.LINK, ChromeVoxState.instance.currentRange.start.node.role);
+  assertEquals('inner', ChromeVoxState.instance.currentRange.start.node.name);
+  mockFeedback.call(doCmd('nextInvalidItem'))
+      .expectSpeech('txet', 'misspelled')
+      .call(doCmd('nextInvalidItem'))
+      .expectSpeech('some other reason')
+      .call(doCmd('nextInvalidItem'))
+      .expectSpeech('this are', 'grammar error')
+      .call(doCmd('nextInvalidItem'))
+      .expectSpeech('error is this')
+      // Ensure wrap.
+      .call(doCmd('nextInvalidItem'))
+      .expectSpeech('txet')
+      // Wrap backward.
+      .call(doCmd('previousInvalidItem'))
+      .expectSpeech('error is this')
+      .call(doCmd('previousInvalidItem'))
+      .expectSpeech('this are', 'grammar error');
 
-    mockFeedback.replay();
-  });
+  mockFeedback.replay();
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'InvalidItemNavigationNoItem', function() {
-  const mockFeedback = this.createMockFeedback();
-  const site = `
+TEST_F(
+    'ChromeVoxBackgroundTest', 'InvalidItemNavigationNoItem', async function() {
+      const mockFeedback = this.createMockFeedback();
+      const site = `
     <h3><a href="#a">inner</a></h3>
     <p>some text</p>
     <button>some other text</button>
     <a href="#b">outer1</a>
     <h3>outer2</h3>
   `;
-  this.runWithLoadedTree(site, function(root) {
-    assertEquals(
-        RoleType.LINK, ChromeVoxState.instance.currentRange.start.node.role);
-    assertEquals('inner', ChromeVoxState.instance.currentRange.start.node.name);
-    mockFeedback.call(doCmd('nextInvalidItem'))
-        .expectSpeech('No invalid item')
-        .call(doCmd('previousInvalidItem'))
-        .expectSpeech('No invalid item');
+      const root = await this.runWithLoadedTree(site);
+      assertEquals(
+          RoleType.LINK, ChromeVoxState.instance.currentRange.start.node.role);
+      assertEquals(
+          'inner', ChromeVoxState.instance.currentRange.start.node.name);
+      mockFeedback.call(doCmd('nextInvalidItem'))
+          .expectSpeech('No invalid item')
+          .call(doCmd('previousInvalidItem'))
+          .expectSpeech('No invalid item');
 
-    mockFeedback.replay();
-  });
-});
+      mockFeedback.replay();
+    });
 
-TEST_F('ChromeVoxBackgroundTest', 'TableWithAriaRowCol', function() {
+TEST_F('ChromeVoxBackgroundTest', 'TableWithAriaRowCol', async function() {
   const mockFeedback = this.createMockFeedback();
   const site = `
     <div role="table">
@@ -2250,31 +2187,30 @@
       </div>
     </div>
   `;
-  this.runWithLoadedTree(site, function(root) {
-    mockFeedback.call(doCmd('fullyDescribe'))
-        .expectSpeech('test', 'row 3 column 1', 'Table , 1 by 1')
-        .replay();
-  });
+  const root = await this.runWithLoadedTree(site);
+  mockFeedback.call(doCmd('fullyDescribe'))
+      .expectSpeech('test', 'row 3 column 1', 'Table , 1 by 1')
+      .replay();
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'NonModalDialogHeadingJump', function() {
-  const mockFeedback = this.createMockFeedback();
-  const site = `
+TEST_F(
+    'ChromeVoxBackgroundTest', 'NonModalDialogHeadingJump', async function() {
+      const mockFeedback = this.createMockFeedback();
+      const site = `
     <h2>Heading outside dialog</h2>
     <div role="dialog">
       <h2>Heading inside dialog</h2>
     </div>
   `;
-  this.runWithLoadedTree(site, function(root) {
-    mockFeedback.call(doCmd('nextHeading'))
-        .expectSpeech('Heading inside dialog')
-        .call(doCmd('previousHeading'))
-        .expectSpeech('Heading outside dialog')
-        .replay();
-  });
-});
+      const root = await this.runWithLoadedTree(site);
+      mockFeedback.call(doCmd('nextHeading'))
+          .expectSpeech('Heading inside dialog')
+          .call(doCmd('previousHeading'))
+          .expectSpeech('Heading outside dialog')
+          .replay();
+    });
 
-TEST_F('ChromeVoxBackgroundTest', 'LevelEndsForNestedLists', function() {
+TEST_F('ChromeVoxBackgroundTest', 'LevelEndsForNestedLists', async function() {
   const mockFeedback = this.createMockFeedback();
   const site = `
     <div>
@@ -2301,84 +2237,82 @@
     </div>
   `;
 
-  this.runWithLoadedTree(site, function(root) {
-    const blueberries = root.find({attributes: {name: 'Blueberries'}});
-    const grapefruits = root.find({attributes: {name: 'Grapefruits'}});
+  const root = await this.runWithLoadedTree(site);
+  const blueberries = root.find({attributes: {name: 'Blueberries'}});
+  const grapefruits = root.find({attributes: {name: 'Grapefruits'}});
 
-    mockFeedback
-        .call(() => {
-          ChromeVoxState.instance.setCurrentRange(
-              cursors.Range.fromNode(blueberries));
-        })
-        .call(doCmd('nextObject'))
-        .expectSpeech(
-            '◦ Raspberries', 'List item', 'List end', 'nested level 2')
-        .call(() => {
-          ChromeVoxState.instance.setCurrentRange(
-              cursors.Range.fromNode(grapefruits));
-        })
-        .call(doCmd('nextObject'))
-        .expectSpeech('■ Mandarins', 'List item', 'List end', 'nested level 3')
-        .call(doCmd('nextObject'))
-        // Nested level is not mentioned for level 1.
-        .expectSpeech('• Bananas', 'List item', 'List end')
-        .replay();
-  });
+  mockFeedback
+      .call(() => {
+        ChromeVoxState.instance.setCurrentRange(
+            cursors.Range.fromNode(blueberries));
+      })
+      .call(doCmd('nextObject'))
+      .expectSpeech('◦ Raspberries', 'List item', 'List end', 'nested level 2')
+      .call(() => {
+        ChromeVoxState.instance.setCurrentRange(
+            cursors.Range.fromNode(grapefruits));
+      })
+      .call(doCmd('nextObject'))
+      .expectSpeech('■ Mandarins', 'List item', 'List end', 'nested level 3')
+      .call(doCmd('nextObject'))
+      // Nested level is not mentioned for level 1.
+      .expectSpeech('• Bananas', 'List item', 'List end')
+      .replay();
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'NestedListNavigationSimple', function() {
-  const mockFeedback = this.createMockFeedback();
-  this.runWithLoadedTree(this.nestedListDoc, function(root) {
-    mockFeedback.expectSpeech('• Lemons', 'List item', 'List', 'with 4 items')
-        .call(doCmd('nextObject'))
-        .expectSpeech('• Oranges', 'List item')
-        .call(doCmd('nextObject'))
-        .expectSpeech('• ', 'Berries', 'List item')
-        .expectBraille('• Berries lstitm')
-        .call(doCmd('nextObject'))
-        .expectSpeech('◦ Strawberries', 'List item', 'List', 'with 2 items')
-        .call(doCmd('nextObject'))
-        .expectSpeech('◦ Raspberries', 'List item', 'List end')
-        .call(doCmd('nextObject'))
-        .expectSpeech('• Bananas', 'List item', 'List end')
-        .expectBraille('• Bananas lstitm lst end')
-        .replay();
-  });
-});
+TEST_F(
+    'ChromeVoxBackgroundTest', 'NestedListNavigationSimple', async function() {
+      const mockFeedback = this.createMockFeedback();
+      const root = await this.runWithLoadedTree(this.nestedListDoc);
+      mockFeedback.expectSpeech('• Lemons', 'List item', 'List', 'with 4 items')
+          .call(doCmd('nextObject'))
+          .expectSpeech('• Oranges', 'List item')
+          .call(doCmd('nextObject'))
+          .expectSpeech('• ', 'Berries', 'List item')
+          .expectBraille('• Berries lstitm')
+          .call(doCmd('nextObject'))
+          .expectSpeech('◦ Strawberries', 'List item', 'List', 'with 2 items')
+          .call(doCmd('nextObject'))
+          .expectSpeech('◦ Raspberries', 'List item', 'List end')
+          .call(doCmd('nextObject'))
+          .expectSpeech('• Bananas', 'List item', 'List end')
+          .expectBraille('• Bananas lstitm lst end')
+          .replay();
+    });
 
-TEST_F('ChromeVoxBackgroundTest', 'NestedListNavigationMixed', function() {
-  const mockFeedback = this.createMockFeedback();
-  this.runWithLoadedTree(this.nestedListDoc, function(root) {
-    mockFeedback.expectSpeech('• Lemons', 'List item', 'List', 'with 4 items')
-        .call(doCmd('nextObject'))
-        .expectSpeech('• Oranges', 'List item')
-        .call(doCmd('nextLine'))
-        .expectSpeech('• ', 'Berries', 'List item')
-        .call(doCmd('nextLine'))
-        .expectSpeech('◦ Strawberries', 'List item', 'List', 'with 2 items')
-        .call(doCmd('previousLine'))
-        .expectSpeech('• ', 'Berries')
-        .call(doCmd('nextWord'))
-        .expectSpeech('◦ Strawberries')
-        .call(doCmd('nextWord'))
-        .expectSpeech('◦ Raspberries')
-        .call(doCmd('previousObject'))
-        .call(doCmd('previousObject'))
-        .expectSpeech('• ', 'Berries')
-        .call(doCmd('previousCharacter'))
-        .call(doCmd('previousCharacter'))
-        .call(doCmd('previousCharacter'))
-        .expectSpeech('g')  // For Oranges
-        .call(doCmd('nextGroup'))
-        .expectSpeech('◦ Strawberries', '◦ Raspberries')
-        .clearPendingOutput()
-        .call(doCmd('previousGroup'))
-        .expectSpeech('• Oranges')
-        .replay();
-  });
-});
+TEST_F(
+    'ChromeVoxBackgroundTest', 'NestedListNavigationMixed', async function() {
+      const mockFeedback = this.createMockFeedback();
+      const root = await this.runWithLoadedTree(this.nestedListDoc);
+      mockFeedback.expectSpeech('• Lemons', 'List item', 'List', 'with 4 items')
+          .call(doCmd('nextObject'))
+          .expectSpeech('• Oranges', 'List item')
+          .call(doCmd('nextLine'))
+          .expectSpeech('• ', 'Berries', 'List item')
+          .call(doCmd('nextLine'))
+          .expectSpeech('◦ Strawberries', 'List item', 'List', 'with 2 items')
+          .call(doCmd('previousLine'))
+          .expectSpeech('• ', 'Berries')
+          .call(doCmd('nextWord'))
+          .expectSpeech('◦ Strawberries')
+          .call(doCmd('nextWord'))
+          .expectSpeech('◦ Raspberries')
+          .call(doCmd('previousObject'))
+          .call(doCmd('previousObject'))
+          .expectSpeech('• ', 'Berries')
+          .call(doCmd('previousCharacter'))
+          .call(doCmd('previousCharacter'))
+          .call(doCmd('previousCharacter'))
+          .expectSpeech('g')  // For Oranges
+          .call(doCmd('nextGroup'))
+          .expectSpeech('◦ Strawberries', '◦ Raspberries')
+          .clearPendingOutput()
+          .call(doCmd('previousGroup'))
+          .expectSpeech('• Oranges')
+          .replay();
+    });
 
-TEST_F('ChromeVoxBackgroundTest', 'NavigationByList', function() {
+TEST_F('ChromeVoxBackgroundTest', 'NavigationByList', async function() {
   const mockFeedback = this.createMockFeedback();
   const site = `
     <p>Start here</p>
@@ -2410,176 +2344,167 @@
       <dd>Description for green</dd>
     </dl>
   `;
-  this.runWithLoadedTree(site, function(root) {
-    mockFeedback.call(doCmd('jumpToTop'))
-        .call(doCmd('nextList'))
-        .expectSpeech('Drinks', 'List', 'with 2 items')
-        .call(doCmd('nextList'))
-        .expectSpeech('List', 'with 0 items')
-        .call(doCmd('nextList'))
-        .expectSpeech('Lunch', 'List', 'with 3 items')
-        .call(doCmd('nextList'))
-        .expectSpeech('Nested list', 'List', 'with 1 item')
-        .call(doCmd('nextList'))
-        .expectSpeech('Colors', 'Description list', 'with 3 items')
-        .call(doCmd('nextList'))
-        // Ensure we wrap correctly.
-        .expectSpeech('Drinks', 'List', 'with 2 items')
-        .call(doCmd('nextObject'))
-        .call(doCmd('nextObject'))
-        .expectSpeech('\u2022 Coffee')
-        // Ensure we wrap correctly and go to previous list, not top of
-        // current list.
-        .call(doCmd('previousList'))
-        .expectSpeech('Colors')
-        .call(doCmd('previousObject'))
-        .expectSpeech('Another random paragraph')
-        // Ensure we dive into the nested list.
-        .call(doCmd('previousList'))
-        .expectSpeech('Nested list', 'List', 'with 1 item')
-        .call(doCmd('previousList'))
-        .expectSpeech('Lunch')
-        .call(doCmd('nextObject'))
-        .call(doCmd('nextObject'))
-        .expectSpeech('1. Burgers')
-        // Ensure we go to the previous list, not the top of the current
-        // list.
-        .call(doCmd('previousList'))
-        .expectSpeech('List', 'with 0 items')
-        .call(doCmd('previousObject'))
-        .expectSpeech('A random paragraph')
-        .call(doCmd('previousList'))
-        .expectSpeech('Drinks', 'List', 'with 2 items')
-        .replay();
-  });
+  const root = await this.runWithLoadedTree(site);
+  mockFeedback.call(doCmd('jumpToTop'))
+      .call(doCmd('nextList'))
+      .expectSpeech('Drinks', 'List', 'with 2 items')
+      .call(doCmd('nextList'))
+      .expectSpeech('List', 'with 0 items')
+      .call(doCmd('nextList'))
+      .expectSpeech('Lunch', 'List', 'with 3 items')
+      .call(doCmd('nextList'))
+      .expectSpeech('Nested list', 'List', 'with 1 item')
+      .call(doCmd('nextList'))
+      .expectSpeech('Colors', 'Description list', 'with 3 items')
+      .call(doCmd('nextList'))
+      // Ensure we wrap correctly.
+      .expectSpeech('Drinks', 'List', 'with 2 items')
+      .call(doCmd('nextObject'))
+      .call(doCmd('nextObject'))
+      .expectSpeech('\u2022 Coffee')
+      // Ensure we wrap correctly and go to previous list, not top of
+      // current list.
+      .call(doCmd('previousList'))
+      .expectSpeech('Colors')
+      .call(doCmd('previousObject'))
+      .expectSpeech('Another random paragraph')
+      // Ensure we dive into the nested list.
+      .call(doCmd('previousList'))
+      .expectSpeech('Nested list', 'List', 'with 1 item')
+      .call(doCmd('previousList'))
+      .expectSpeech('Lunch')
+      .call(doCmd('nextObject'))
+      .call(doCmd('nextObject'))
+      .expectSpeech('1. Burgers')
+      // Ensure we go to the previous list, not the top of the current
+      // list.
+      .call(doCmd('previousList'))
+      .expectSpeech('List', 'with 0 items')
+      .call(doCmd('previousObject'))
+      .expectSpeech('A random paragraph')
+      .call(doCmd('previousList'))
+      .expectSpeech('Drinks', 'List', 'with 2 items')
+      .replay();
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'NoListTest', function() {
+TEST_F('ChromeVoxBackgroundTest', 'NoListTest', async function() {
   const mockFeedback = this.createMockFeedback();
-  this.runWithLoadedTree('<button>Click me</button>', function(root) {
-    mockFeedback.call(doCmd('nextList'))
-        .expectSpeech('No next list')
-        .call(doCmd('previousList'))
-        .expectSpeech('No previous list');
-    mockFeedback.replay();
-  });
+  const root = await this.runWithLoadedTree('<button>Click me</button>');
+  mockFeedback.call(doCmd('nextList'))
+      .expectSpeech('No next list')
+      .call(doCmd('previousList'))
+      .expectSpeech('No previous list');
+  mockFeedback.replay();
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'NavigateToLastHeading', function() {
+TEST_F('ChromeVoxBackgroundTest', 'NavigateToLastHeading', async function() {
   const mockFeedback = this.createMockFeedback();
   const site = `
     <h1>First</h1>
     <h1>Second</h1>
     <h1>Third</h1>
   `;
-  this.runWithLoadedTree(site, function(root) {
-    mockFeedback.call(doCmd('jumpToTop'))
-        .expectSpeech('First', 'Heading 1')
-        .call(doCmd('previousHeading'))
-        .expectSpeech('Third', 'Heading 1')
-        .replay();
-  });
+  const root = await this.runWithLoadedTree(site);
+  mockFeedback.call(doCmd('jumpToTop'))
+      .expectSpeech('First', 'Heading 1')
+      .call(doCmd('previousHeading'))
+      .expectSpeech('Third', 'Heading 1')
+      .replay();
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'ReadLinkURLTest', function() {
+TEST_F('ChromeVoxBackgroundTest', 'ReadLinkURLTest', async function() {
   const mockFeedback = this.createMockFeedback();
   const site = `
     <a href="https://www.google.com/">A popular link</a>
     <button>Not a link</button>
   `;
-  this.runWithLoadedTree(site, function(root) {
-    mockFeedback.call(doCmd('nextLink'))
-        .expectSpeech(
-            'A popular link', 'Link', 'Press Search+Space to activate')
-        .call(doCmd('readLinkURL'))
-        .expectSpeech('Link URL: https://www.google.com/')
-        .call(doCmd('nextObject'))
-        .expectSpeech('Not a link', 'Button', 'Press Search+Space to activate')
-        .call(doCmd('readLinkURL'))
-        .expectSpeech('No URL found')
-        .replay();
-  });
+  const root = await this.runWithLoadedTree(site);
+  mockFeedback.call(doCmd('nextLink'))
+      .expectSpeech('A popular link', 'Link', 'Press Search+Space to activate')
+      .call(doCmd('readLinkURL'))
+      .expectSpeech('Link URL: https://www.google.com/')
+      .call(doCmd('nextObject'))
+      .expectSpeech('Not a link', 'Button', 'Press Search+Space to activate')
+      .call(doCmd('readLinkURL'))
+      .expectSpeech('No URL found')
+      .replay();
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'NoRepeatTitle', function() {
+TEST_F('ChromeVoxBackgroundTest', 'NoRepeatTitle', async function() {
   const mockFeedback = this.createMockFeedback();
   const site = `
     <div role="button" aria-label="title" title="title"></div>
   `;
-  this.runWithLoadedTree(site, function(root) {
-    mockFeedback.expectSpeech('title')
-        .expectSpeech('Button')
-        .expectNextSpeechUtteranceIsNot('title')
-        .expectSpeech('Press Search+Space to activate')
-        .replay();
-  });
+  const root = await this.runWithLoadedTree(site);
+  mockFeedback.expectSpeech('title')
+      .expectSpeech('Button')
+      .expectNextSpeechUtteranceIsNot('title')
+      .expectSpeech('Press Search+Space to activate')
+      .replay();
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'PhoneticsAndCommands', function() {
+TEST_F('ChromeVoxBackgroundTest', 'PhoneticsAndCommands', async function() {
   const mockFeedback = this.createMockFeedback();
   const site = `
     <p>some sample text</p>
     <button>ok</button>
     <p>A</p>
   `;
-  this.runWithLoadedTree(site, function(root) {
-    const noPhonetics = {phoneticCharacters: undefined};
-    const phonetics = {phoneticCharacters: true};
-    mockFeedback.call(doCmd('nextObject'))
-        .expectSpeechWithProperties(noPhonetics, 'ok')
-        .call(doCmd('previousObject'))
-        .expectSpeechWithProperties(noPhonetics, 'some sample text')
-        .call(doCmd('nextWord'))
-        .expectSpeechWithProperties(noPhonetics, 'sample')
-        .call(doCmd('previousWord'))
-        .expectSpeechWithProperties(noPhonetics, 'some')
-        .call(doCmd('nextCharacter'))
-        .expectSpeechWithProperties(phonetics, 'o')
-        .call(doCmd('nextCharacter'))
-        .expectSpeechWithProperties(phonetics, 'm')
-        .call(doCmd('previousCharacter'))
-        .expectSpeechWithProperties(phonetics, 'o')
-        .call(doCmd('jumpToBottom'))
-        .expectSpeechWithProperties(noPhonetics, 'A');
-    mockFeedback.replay();
-  });
+  const root = await this.runWithLoadedTree(site);
+  const noPhonetics = {phoneticCharacters: undefined};
+  const phonetics = {phoneticCharacters: true};
+  mockFeedback.call(doCmd('nextObject'))
+      .expectSpeechWithProperties(noPhonetics, 'ok')
+      .call(doCmd('previousObject'))
+      .expectSpeechWithProperties(noPhonetics, 'some sample text')
+      .call(doCmd('nextWord'))
+      .expectSpeechWithProperties(noPhonetics, 'sample')
+      .call(doCmd('previousWord'))
+      .expectSpeechWithProperties(noPhonetics, 'some')
+      .call(doCmd('nextCharacter'))
+      .expectSpeechWithProperties(phonetics, 'o')
+      .call(doCmd('nextCharacter'))
+      .expectSpeechWithProperties(phonetics, 'm')
+      .call(doCmd('previousCharacter'))
+      .expectSpeechWithProperties(phonetics, 'o')
+      .call(doCmd('jumpToBottom'))
+      .expectSpeechWithProperties(noPhonetics, 'A');
+  mockFeedback.replay();
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'ToggleScreen', function() {
+TEST_F('ChromeVoxBackgroundTest', 'ToggleScreen', async function() {
   const mockFeedback = this.createMockFeedback();
   // Pretend we've already accepted the confirmation dialog once.
   localStorage['acceptToggleScreen'] = 'true';
-  this.runWithLoadedTree('<div>Unimportant web content</div>', function() {
-    mockFeedback.call(doCmd('toggleScreen'))
-        .expectSpeech('Screen off')
-        .call(doCmd('toggleScreen'))
-        .expectSpeech('Screen on')
-        .call(doCmd('toggleScreen'))
-        .expectSpeech('Screen off')
-        .replay();
-  });
+  await this.runWithLoadedTree('<div>Unimportant web content</div>');
+  mockFeedback.call(doCmd('toggleScreen'))
+      .expectSpeech('Screen off')
+      .call(doCmd('toggleScreen'))
+      .expectSpeech('Screen on')
+      .call(doCmd('toggleScreen'))
+      .expectSpeech('Screen off')
+      .replay();
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'NoFocusTalkBackDisabled', function() {
+TEST_F('ChromeVoxBackgroundTest', 'NoFocusTalkBackDisabled', async function() {
   // Fire onCustomSpokenFeedbackEnabled event to communicate that Talkback is
   // off for the current app.
   this.dispatchOnCustomSpokenFeedbackToggledEvent(false);
   const mockFeedback = this.createMockFeedback();
-  this.runWithLoadedTree('<p>Test document</p>', function() {
-    ChromeVoxState.instance.setCurrentRange(null);
-    mockFeedback.call(doCmd('nextObject'))
-        .expectSpeech(
-            'No current ChromeVox focus. Press Alt+Shift+L to go to the ' +
-            'launcher.')
-        .call(doCmd('previousObject'))
-        .expectSpeech(
-            'No current ChromeVox focus. Press Alt+Shift+L to go to the ' +
-            'launcher.');
-    mockFeedback.replay();
-  });
+  await this.runWithLoadedTree('<p>Test document</p>');
+  ChromeVoxState.instance.setCurrentRange(null);
+  mockFeedback.call(doCmd('nextObject'))
+      .expectSpeech(
+          'No current ChromeVox focus. Press Alt+Shift+L to go to the ' +
+          'launcher.')
+      .call(doCmd('previousObject'))
+      .expectSpeech(
+          'No current ChromeVox focus. Press Alt+Shift+L to go to the ' +
+          'launcher.');
+  mockFeedback.replay();
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'NoFocusTalkBackEnabled', function() {
+TEST_F('ChromeVoxBackgroundTest', 'NoFocusTalkBackEnabled', async function() {
   // Fire onCustomSpokenFeedbackEnabled event to communicate that Talkback is
   // on for the current app. We don't want to announce the no-focus hint message
   // when TalkBack is on because we expect ChromeVox to have no focus in that
@@ -2587,23 +2512,22 @@
   // try to speak at the same time.
   this.dispatchOnCustomSpokenFeedbackToggledEvent(true);
   const mockFeedback = this.createMockFeedback();
-  this.runWithLoadedTree('<p>Start here</p>', function() {
-    ChromeVoxState.instance.setCurrentRange(null);
-    mockFeedback.call(doCmd('nextObject'))
-        .call(
-            () => assertFalse(mockFeedback.utteranceInQueue(
-                'No current ChromeVox focus. ' +
-                'Press Alt+Shift+L to go to the launcher.')))
-        .call(doCmd('previousObject'))
-        .call(
-            () => assertFalse(mockFeedback.utteranceInQueue(
-                'No current ChromeVox focus. ' +
-                'Press Alt+Shift+L to go to the launcher.')));
-    mockFeedback.replay();
-  });
+  await this.runWithLoadedTree('<p>Start here</p>');
+  ChromeVoxState.instance.setCurrentRange(null);
+  mockFeedback.call(doCmd('nextObject'))
+      .call(
+          () => assertFalse(mockFeedback.utteranceInQueue(
+              'No current ChromeVox focus. ' +
+              'Press Alt+Shift+L to go to the launcher.')))
+      .call(doCmd('previousObject'))
+      .call(
+          () => assertFalse(mockFeedback.utteranceInQueue(
+              'No current ChromeVox focus. ' +
+              'Press Alt+Shift+L to go to the launcher.')));
+  mockFeedback.replay();
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'NavigateOutOfMultiline', function() {
+TEST_F('ChromeVoxBackgroundTest', 'NavigateOutOfMultiline', async function() {
   const mockFeedback = this.createMockFeedback();
   const site = `
     <p>start</p>
@@ -2613,33 +2537,32 @@
     </div>
     <p>after</p>
   `;
-  this.runWithLoadedTree(site, function(root) {
-    const contentEditable =
-        root.find({attributes: {nonAtomicTextFieldRoot: true}});
-    mockFeedback.call(contentEditable.focus.bind(contentEditable))
-        .expectSpeech(/Testing testing\s+one two three/)
-        .call(doCmd('nextLine'))
-        .expectSpeech('one two three')
-        .call(doCmd('nextLine'))
-        .expectSpeech('after')
+  const root = await this.runWithLoadedTree(site);
+  const contentEditable =
+      root.find({attributes: {nonAtomicTextFieldRoot: true}});
+  mockFeedback.call(contentEditable.focus.bind(contentEditable))
+      .expectSpeech(/Testing testing\s+one two three/)
+      .call(doCmd('nextLine'))
+      .expectSpeech('one two three')
+      .call(doCmd('nextLine'))
+      .expectSpeech('after')
 
-        // In reverse (explicitly focus, instead of moving to previous
-        // line, because all subsequent commands require the content
-        // editable to be focused first):
-        .clearPendingOutput()
-        .call(contentEditable.focus.bind(contentEditable))
-        .expectSpeech(/Testing testing\s+one two three/)
-        .call(doCmd('nextLine'))
-        .expectSpeech('one two three')
-        .call(doCmd('previousLine'))
-        .expectSpeech('Testing testing')
-        .call(doCmd('previousLine'))
-        .expectSpeech('before')
-        .replay();
-  });
+      // In reverse (explicitly focus, instead of moving to previous
+      // line, because all subsequent commands require the content
+      // editable to be focused first):
+      .clearPendingOutput()
+      .call(contentEditable.focus.bind(contentEditable))
+      .expectSpeech(/Testing testing\s+one two three/)
+      .call(doCmd('nextLine'))
+      .expectSpeech('one two three')
+      .call(doCmd('previousLine'))
+      .expectSpeech('Testing testing')
+      .call(doCmd('previousLine'))
+      .expectSpeech('before')
+      .replay();
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'ReadWindowTitle', function() {
+TEST_F('ChromeVoxBackgroundTest', 'ReadWindowTitle', async function() {
   const mockFeedback = this.createMockFeedback();
   const site = `
     <p>start</p>
@@ -2649,62 +2572,59 @@
       button.addEventListener('click', _ => document.title = 'bar');
     </script>
   `;
-  this.runWithLoadedTree(site, function(root) {
-    const clickButtonThenReadCurrentTitle = () => {
-      const desktop = root.parent.root;
-      desktop.addEventListener(EventType.TREE_CHANGED, (evt) => {
-        if (evt.target.role === RoleType.WINDOW &&
-            /bar/.test(evt.target.name)) {
-          doCmd('readCurrentTitle')();
-        }
-      });
-      const button = root.find({role: RoleType.BUTTON});
-      button.doDefault();
-    };
+  const root = await this.runWithLoadedTree(site);
+  const clickButtonThenReadCurrentTitle = () => {
+    const desktop = root.parent.root;
+    desktop.addEventListener(EventType.TREE_CHANGED, (evt) => {
+      if (evt.target.role === RoleType.WINDOW && /bar/.test(evt.target.name)) {
+        doCmd('readCurrentTitle')();
+      }
+    });
+    const button = root.find({role: RoleType.BUTTON});
+    button.doDefault();
+  };
 
-    mockFeedback.clearPendingOutput()
-        .call(clickButtonThenReadCurrentTitle)
+  mockFeedback.clearPendingOutput()
+      .call(clickButtonThenReadCurrentTitle)
 
-        // This test may run against official builds, so match against
-        // utterances starting with 'bar'. This should exclude any other
-        // utterances that contain 'bar' e.g. data:...bar.. or the data url.
-        .expectSpeech(/^bar*/)
-        .replay();
-  });
+      // This test may run against official builds, so match against
+      // utterances starting with 'bar'. This should exclude any other
+      // utterances that contain 'bar' e.g. data:...bar.. or the data url.
+      .expectSpeech(/^bar*/)
+      .replay();
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'OutputEmptyQueueMode', function() {
+TEST_F('ChromeVoxBackgroundTest', 'OutputEmptyQueueMode', async function() {
   const mockFeedback = this.createMockFeedback();
-  this.runWithLoadedTree('<p>unused</p>', function(root) {
-    const output = new Output();
-    Output.forceModeForNextSpeechUtterance(QueueMode.CATEGORY_FLUSH);
-    output.append_(
-        output.speechBuffer_, new Spannable(''),
-        {annotation: [new OutputAction()]});
-    output.withString('test');
-    mockFeedback.clearPendingOutput()
-        .call(output.go.bind(output))
-        .expectSpeechWithQueueMode('', QueueMode.CATEGORY_FLUSH)
-        .expectSpeechWithQueueMode('test', QueueMode.CATEGORY_FLUSH)
-        .replay();
-  });
+  const root = await this.runWithLoadedTree('<p>unused</p>');
+  const output = new Output();
+  Output.forceModeForNextSpeechUtterance(QueueMode.CATEGORY_FLUSH);
+  output.append_(
+      output.speechBuffer_, new Spannable(''),
+      {annotation: [new OutputAction()]});
+  output.withString('test');
+  mockFeedback.clearPendingOutput()
+      .call(output.go.bind(output))
+      .expectSpeechWithQueueMode('', QueueMode.CATEGORY_FLUSH)
+      .expectSpeechWithQueueMode('test', QueueMode.CATEGORY_FLUSH)
+      .replay();
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'SetAccessibilityFocus', function() {
-  this.runWithLoadedTree('<p>Text.</p><button>Button</button>', function(root) {
-    const node = root.find({role: RoleType.BUTTON});
+TEST_F('ChromeVoxBackgroundTest', 'SetAccessibilityFocus', async function() {
+  const root =
+      await this.runWithLoadedTree('<p>Text.</p><button>Button</button>');
+  const node = root.find({role: RoleType.BUTTON});
 
-    node.addEventListener(EventType.FOCUS, this.newCallback(function() {
-      chrome.automation.getAccessibilityFocus((focusedNode) => {
-        assertEquals(node, focusedNode);
-      });
-    }));
+  node.addEventListener(EventType.FOCUS, this.newCallback(function() {
+    chrome.automation.getAccessibilityFocus((focusedNode) => {
+      assertEquals(node, focusedNode);
+    });
+  }));
 
-    node.focus();
-  });
+  node.focus();
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'MenuItemRadio', function() {
+TEST_F('ChromeVoxBackgroundTest', 'MenuItemRadio', async function() {
   const mockFeedback = this.createMockFeedback();
   const site = `
     <ul role="menu" tabindex="0" autofocus>
@@ -2713,19 +2633,18 @@
       <li role="menuitemradio" aria-checked="false">Large</li>
     </ul>
   `;
-  this.runWithLoadedTree(site, function(root) {
-    mockFeedback.expectSpeech('Menu', 'with 3 items')
-        .call(doCmd('nextObject'))
-        .expectSpeech('Small, menu item radio button selected', ' 1 of 3 ')
-        .call(doCmd('nextObject'))
-        .expectSpeech('Medium, menu item radio button unselected', ' 2 of 3 ')
-        .replay();
-  });
+  const root = await this.runWithLoadedTree(site);
+  mockFeedback.expectSpeech('Menu', 'with 3 items')
+      .call(doCmd('nextObject'))
+      .expectSpeech('Small, menu item radio button selected', ' 1 of 3 ')
+      .call(doCmd('nextObject'))
+      .expectSpeech('Medium, menu item radio button unselected', ' 2 of 3 ')
+      .replay();
 });
 
 TEST_F(
     'ChromeVoxBackgroundTest', 'ButtonNavigationIgnoresRadioButtons',
-    function() {
+    async function() {
       const mockFeedback = this.createMockFeedback();
       const site = `
         <button>Action 1</button>
@@ -2736,63 +2655,59 @@
         <button>Action 2</button>
       `;
 
-      this.runWithLoadedTree(site, function(root) {
-        mockFeedback.call(doCmd('nextButton'))
-            .expectSpeech('Action 1', 'Button')
-            .call(doCmd('nextButton'))
-            .expectSpeech('Action 2', 'Button');
+      const root = await this.runWithLoadedTree(site);
+      mockFeedback.call(doCmd('nextButton'))
+          .expectSpeech('Action 1', 'Button')
+          .call(doCmd('nextButton'))
+          .expectSpeech('Action 2', 'Button');
 
-        mockFeedback.replay();
-      });
+      mockFeedback.replay();
     });
 
 TEST_F(
-    'ChromeVoxBackgroundTest', 'FocusableNamedDivIsNotContainer', function() {
+    'ChromeVoxBackgroundTest', 'FocusableNamedDivIsNotContainer',
+    async function() {
       const site = `
         <div aria-label="hello world" tabindex="0">hello world</div>
       `;
-      this.runWithLoadedTree(site, function(root) {
-        const genericContainer = root.find({role: RoleType.GENERIC_CONTAINER});
-        assertTrue(AutomationPredicate.object(genericContainer));
-        assertFalse(AutomationPredicate.container(genericContainer));
-      });
+      const root = await this.runWithLoadedTree(site);
+      const genericContainer = root.find({role: RoleType.GENERIC_CONTAINER});
+      assertTrue(AutomationPredicate.object(genericContainer));
+      assertFalse(AutomationPredicate.container(genericContainer));
     });
 
-TEST_F('ChromeVoxBackgroundTest', 'HitTestOnExoSurface', function() {
+TEST_F('ChromeVoxBackgroundTest', 'HitTestOnExoSurface', async function() {
   const site = `
     <button></button>
     <input type="text"</input>
   `;
-  this.runWithLoadedTree(site, function(root) {
-    const fakeWindow = root.find({role: RoleType.BUTTON});
-    const realTextField = root.find({role: RoleType.TEXT_FIELD});
+  const root = await this.runWithLoadedTree(site);
+  const fakeWindow = root.find({role: RoleType.BUTTON});
+  const realTextField = root.find({role: RoleType.TEXT_FIELD});
 
-    // Fake the role and className to imitate a ExoSurface.
-    Object.defineProperty(fakeWindow, 'role', {get: () => RoleType.WINDOW});
-    Object.defineProperty(
-        fakeWindow, 'className', {get: () => 'ExoSurface-40'});
+  // Fake the role and className to imitate a ExoSurface.
+  Object.defineProperty(fakeWindow, 'role', {get: () => RoleType.WINDOW});
+  Object.defineProperty(fakeWindow, 'className', {get: () => 'ExoSurface-40'});
 
-    // Mock and expect a call for the fake window.
-    chrome.accessibilityPrivate.sendSyntheticMouseEvent =
-        this.newCallback(evt => {
-          assertEquals(fakeWindow.location.left, evt.x);
-          assertEquals(fakeWindow.location.top, evt.y);
-        });
+  // Mock and expect a call for the fake window.
+  chrome.accessibilityPrivate.sendSyntheticMouseEvent =
+      this.newCallback(evt => {
+        assertEquals(fakeWindow.location.left, evt.x);
+        assertEquals(fakeWindow.location.top, evt.y);
+      });
 
-    // Fake a mouse explore event on the real text field. This should not
-    // trigger the above mouse path.
-    GestureCommandHandler.pointerHandler_.onMouseMove(
-        realTextField.location.left, realTextField.location.top);
+  // Fake a mouse explore event on the real text field. This should not
+  // trigger the above mouse path.
+  GestureCommandHandler.pointerHandler_.onMouseMove(
+      realTextField.location.left, realTextField.location.top);
 
-    // Fake a touch explore gesture event on the fake window which should
-    // trigger a mouse move.
-    GestureCommandHandler.onAccessibilityGesture_(
-        Gesture.TOUCH_EXPLORE, fakeWindow.location.left,
-        fakeWindow.location.top);
-  });
+  // Fake a touch explore gesture event on the fake window which should
+  // trigger a mouse move.
+  GestureCommandHandler.onAccessibilityGesture_(
+      Gesture.TOUCH_EXPLORE, fakeWindow.location.left, fakeWindow.location.top);
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'PointerSkipsContainers', function() {
+TEST_F('ChromeVoxBackgroundTest', 'PointerSkipsContainers', async function() {
   PointerHandler.MIN_NO_POINTER_ANCHOR_SOUND_DELAY_MS = -1;
   const mockFeedback = this.createMockFeedback();
   const site = `
@@ -2800,43 +2715,42 @@
       <div role=button><p></p></div>
     </div>
   `;
-  this.runWithLoadedTree(site, function(root) {
-    ChromeVoxState.addObserver(new class {
-      onCurrentRangeChanged(range) {
-        if (!range) {
-          ChromeVox.tts.speak('range cleared!');
-        }
+  const root = await this.runWithLoadedTree(site);
+  ChromeVoxState.addObserver(new class {
+    onCurrentRangeChanged(range) {
+      if (!range) {
+        ChromeVox.tts.speak('range cleared!');
       }
-    }());
+    }
+  }());
 
-    const button = root.find({role: RoleType.BUTTON});
-    assertNotNullNorUndefined(button);
-    const group = button.parent;
-    assertNotNullNorUndefined(group);
-    mockFeedback.call(simulateHitTestResult(button))
-        .expectSpeech('Button')
-        .call(() => {
-          // Override the role to simulate panes which are only found in
-          // views.
-          Object.defineProperty(group, 'role', {
-            get() {
-              return chrome.automation.RoleType.PANE;
-            }
-          });
-        })
-        .call(simulateHitTestResult(group))
-        .expectSpeech('range cleared!')
-        .expectEarcon(Earcon.NO_POINTER_ANCHOR)
-        .call(simulateHitTestResult(button))
-        .expectSpeech('Button')
-        .call(simulateHitTestResult(group))
-        .expectSpeech('range cleared!')
-        .expectEarcon(Earcon.NO_POINTER_ANCHOR)
-        .replay();
-  });
+  const button = root.find({role: RoleType.BUTTON});
+  assertNotNullNorUndefined(button);
+  const group = button.parent;
+  assertNotNullNorUndefined(group);
+  mockFeedback.call(simulateHitTestResult(button))
+      .expectSpeech('Button')
+      .call(() => {
+        // Override the role to simulate panes which are only found in
+        // views.
+        Object.defineProperty(group, 'role', {
+          get() {
+            return chrome.automation.RoleType.PANE;
+          }
+        });
+      })
+      .call(simulateHitTestResult(group))
+      .expectSpeech('range cleared!')
+      .expectEarcon(Earcon.NO_POINTER_ANCHOR)
+      .call(simulateHitTestResult(button))
+      .expectSpeech('Button')
+      .call(simulateHitTestResult(group))
+      .expectSpeech('range cleared!')
+      .expectEarcon(Earcon.NO_POINTER_ANCHOR)
+      .replay();
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'FocusOnUnknown', function() {
+TEST_F('ChromeVoxBackgroundTest', 'FocusOnUnknown', async function() {
   const mockFeedback = this.createMockFeedback();
   const site = `
     <p>start</p>
@@ -2845,46 +2759,44 @@
     </div>
     <div role="group" tabindex=0></div>
   `;
-  this.runWithLoadedTree(site, function(root) {
-    const [group1, group2] = root.findAll({role: RoleType.GROUP});
-    assertNotNullNorUndefined(group1);
-    assertNotNullNorUndefined(group2);
-    Object.defineProperty(group1, 'role', {
-      get() {
-        return chrome.automation.RoleType.UNKNOWN;
-      }
-    });
-    Object.defineProperty(group2, 'role', {
-      get() {
-        return chrome.automation.RoleType.UNKNOWN;
-      }
-    });
-
-    const evt2 = new CustomAutomationEvent(EventType.FOCUS, group2);
-    const currentRange = ChromeVoxState.instance.currentRange;
-    DesktopAutomationInterface.instance.onFocus(evt2);
-    assertEquals(currentRange, ChromeVoxState.instance.currentRange);
-
-    const evt1 = new CustomAutomationEvent(EventType.FOCUS, group1);
-    mockFeedback
-        .call(DesktopAutomationInterface.instance.onFocus.bind(
-            DesktopAutomationInterface.instance, evt1))
-        .expectSpeech('hello')
-        .replay();
+  const root = await this.runWithLoadedTree(site);
+  const [group1, group2] = root.findAll({role: RoleType.GROUP});
+  assertNotNullNorUndefined(group1);
+  assertNotNullNorUndefined(group2);
+  Object.defineProperty(group1, 'role', {
+    get() {
+      return chrome.automation.RoleType.UNKNOWN;
+    }
   });
+  Object.defineProperty(group2, 'role', {
+    get() {
+      return chrome.automation.RoleType.UNKNOWN;
+    }
+  });
+
+  const evt2 = new CustomAutomationEvent(EventType.FOCUS, group2);
+  const currentRange = ChromeVoxState.instance.currentRange;
+  DesktopAutomationInterface.instance.onFocus(evt2);
+  assertEquals(currentRange, ChromeVoxState.instance.currentRange);
+
+  const evt1 = new CustomAutomationEvent(EventType.FOCUS, group1);
+  mockFeedback
+      .call(DesktopAutomationInterface.instance.onFocus.bind(
+          DesktopAutomationInterface.instance, evt1))
+      .expectSpeech('hello')
+      .replay();
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'TimeDateCommand', function() {
+TEST_F('ChromeVoxBackgroundTest', 'TimeDateCommand', async function() {
   const mockFeedback = this.createMockFeedback();
-  this.runWithLoadedTree('<p></p>', function(root) {
-    mockFeedback.call(doCmd('speakTimeAndDate'))
-        .expectSpeech(/(AM|PM)*(2)/)
-        .expectBraille(/(AM|PM)*(2)/)
-        .replay();
-  });
+  const root = await this.runWithLoadedTree('<p></p>');
+  mockFeedback.call(doCmd('speakTimeAndDate'))
+      .expectSpeech(/(AM|PM)*(2)/)
+      .expectBraille(/(AM|PM)*(2)/)
+      .replay();
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'SwipeToScrollByPage', function() {
+TEST_F('ChromeVoxBackgroundTest', 'SwipeToScrollByPage', async function() {
   const mockFeedback = this.createMockFeedback();
   const site = `
     <p style="font-size: 200pt">This is a test</p>
@@ -2895,50 +2807,49 @@
     <p style="font-size: 200pt">This is a test</p>
     <p style="font-size: 200pt">This is a test</p>
   `;
-  this.runWithLoadedTree(site, function(root) {
-    mockFeedback.call(doGesture(Gesture.SWIPE_UP3))
-        .expectSpeech(/Page 2 of/)
-        .call(doGesture(Gesture.SWIPE_UP3))
-        .expectSpeech(/Page 3 of/)
-        .call(doGesture(Gesture.SWIPE_DOWN3))
-        .expectSpeech(/Page 2 of/)
-        .call(doGesture(Gesture.SWIPE_DOWN3))
-        .expectSpeech(/Page 1 of/)
-        .replay();
-  });
+  const root = await this.runWithLoadedTree(site);
+  mockFeedback.call(doGesture(Gesture.SWIPE_UP3))
+      .expectSpeech(/Page 2 of/)
+      .call(doGesture(Gesture.SWIPE_UP3))
+      .expectSpeech(/Page 3 of/)
+      .call(doGesture(Gesture.SWIPE_DOWN3))
+      .expectSpeech(/Page 2 of/)
+      .call(doGesture(Gesture.SWIPE_DOWN3))
+      .expectSpeech(/Page 1 of/)
+      .replay();
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'PointerOnOffOnRepeatsNode', function() {
-  PointerHandler.MIN_NO_POINTER_ANCHOR_SOUND_DELAY_MS = -1;
-  const mockFeedback = this.createMockFeedback();
-  this.runWithLoadedTree('<button>hi</button>', function(root) {
-    ChromeVoxState.addObserver(new class {
-      onCurrentRangeChanged(range) {
-        if (!range) {
-          ChromeVox.tts.speak('range cleared!');
+TEST_F(
+    'ChromeVoxBackgroundTest', 'PointerOnOffOnRepeatsNode', async function() {
+      PointerHandler.MIN_NO_POINTER_ANCHOR_SOUND_DELAY_MS = -1;
+      const mockFeedback = this.createMockFeedback();
+      const root = await this.runWithLoadedTree('<button>hi</button>');
+      ChromeVoxState.addObserver(new class {
+        onCurrentRangeChanged(range) {
+          if (!range) {
+            ChromeVox.tts.speak('range cleared!');
+          }
         }
-      }
-    }());
+      }());
 
-    const button = root.find({role: RoleType.BUTTON});
-    assertNotNullNorUndefined(button);
-    mockFeedback.call(simulateHitTestResult(button))
-        .expectSpeech('hi', 'Button')
+      const button = root.find({role: RoleType.BUTTON});
+      assertNotNullNorUndefined(button);
+      mockFeedback.call(simulateHitTestResult(button))
+          .expectSpeech('hi', 'Button')
 
-        // Touch slightly off of the button.
-        .call(GestureCommandHandler.onAccessibilityGesture_.bind(
-            null, Gesture.TOUCH_EXPLORE, button.location.left,
-            button.location.top + 60))
-        .expectSpeech('range cleared!')
-        .expectEarcon(Earcon.NO_POINTER_ANCHOR)
-        .clearPendingOutput()
-        .call(simulateHitTestResult(button))
-        .expectSpeech('hi', 'Button')
-        .replay();
-  });
-});
+          // Touch slightly off of the button.
+          .call(GestureCommandHandler.onAccessibilityGesture_.bind(
+              null, Gesture.TOUCH_EXPLORE, button.location.left,
+              button.location.top + 60))
+          .expectSpeech('range cleared!')
+          .expectEarcon(Earcon.NO_POINTER_ANCHOR)
+          .clearPendingOutput()
+          .call(simulateHitTestResult(button))
+          .expectSpeech('hi', 'Button')
+          .replay();
+    });
 
-TEST_F('ChromeVoxBackgroundTest', 'PopupButtonCollapsed', function() {
+TEST_F('ChromeVoxBackgroundTest', 'PopupButtonCollapsed', async function() {
   const mockFeedback = this.createMockFeedback();
   const site = `
     <select id="button">
@@ -2946,16 +2857,15 @@
       <option value="Banana">Banana</option>
     </select>
   `;
-  this.runWithLoadedTree(site, function(root) {
-    mockFeedback.call(doCmd('jumpToTop'))
-        .expectSpeech(
-            'Apple', 'Button', 'has pop up', 'Collapsed',
-            'Press Search+Space to activate')
-        .replay();
-  });
+  const root = await this.runWithLoadedTree(site);
+  mockFeedback.call(doCmd('jumpToTop'))
+      .expectSpeech(
+          'Apple', 'Button', 'has pop up', 'Collapsed',
+          'Press Search+Space to activate')
+      .replay();
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'PopupButtonExpanded', function() {
+TEST_F('ChromeVoxBackgroundTest', 'PopupButtonExpanded', async function() {
   const mockFeedback = this.createMockFeedback();
   const site = `
     <button id="button" aria-haspopup="true" aria-expanded="true"
@@ -2970,18 +2880,17 @@
       <li role="menuitem">Item 3</li>
     </ul>
   `;
-  this.runWithLoadedTree(site, function(root) {
-    mockFeedback
-        .call(doCmd('jumpToTop'))
-        // SetSize is only reported if popup button is expanded.
-        .expectSpeech(
-            'Click me', 'Button', 'has pop up', 'with 3 items', 'Expanded',
-            'Press Search+Space to activate')
-        .replay();
-  });
+  const root = await this.runWithLoadedTree(site);
+  mockFeedback
+      .call(doCmd('jumpToTop'))
+      // SetSize is only reported if popup button is expanded.
+      .expectSpeech(
+          'Click me', 'Button', 'has pop up', 'with 3 items', 'Expanded',
+          'Press Search+Space to activate')
+      .replay();
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'SortDirection', function() {
+TEST_F('ChromeVoxBackgroundTest', 'SortDirection', async function() {
   const mockFeedback = this.createMockFeedback();
   const site = `
     <table border="1">
@@ -2999,130 +2908,125 @@
       });
     </script>
   `;
-  this.runWithLoadedTree(site, function(root) {
-    const sortButton = root.find({role: RoleType.BUTTON});
-    mockFeedback.expectSpeech('Button', 'Ascending sort')
-        .call(sortButton.doDefault.bind(sortButton))
-        .expectSpeech('Descending sort')
-        .call(sortButton.doDefault.bind(sortButton))
-        .expectSpeech('Ascending sort')
-        .call(sortButton.doDefault.bind(sortButton))
-        .expectSpeech('Descending sort')
-        .replay();
-  });
+  const root = await this.runWithLoadedTree(site);
+  const sortButton = root.find({role: RoleType.BUTTON});
+  mockFeedback.expectSpeech('Button', 'Ascending sort')
+      .call(sortButton.doDefault.bind(sortButton))
+      .expectSpeech('Descending sort')
+      .call(sortButton.doDefault.bind(sortButton))
+      .expectSpeech('Ascending sort')
+      .call(sortButton.doDefault.bind(sortButton))
+      .expectSpeech('Descending sort')
+      .replay();
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'InlineLineNavigation', function() {
+TEST_F('ChromeVoxBackgroundTest', 'InlineLineNavigation', async function() {
   const mockFeedback = this.createMockFeedback();
   const site = `
     <p>start</p>
     <p><strong>This</strong><b>is</b>a <em>test</em></p>
   `;
-  this.runWithLoadedTree(site, function(root) {
-    mockFeedback.call(doCmd('nextLine'))
-        .expectSpeech('This', 'is', 'a ', 'test')
-        .replay();
-  });
+  const root = await this.runWithLoadedTree(site);
+  mockFeedback.call(doCmd('nextLine'))
+      .expectSpeech('This', 'is', 'a ', 'test')
+      .replay();
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'AudioVideo', function() {
+TEST_F('ChromeVoxBackgroundTest', 'AudioVideo', async function() {
   const mockFeedback = this.createMockFeedback();
   const site = `
     <button></button>
     <button></button>
   `;
-  this.runWithLoadedTree(site, function(root) {
-    const [audio, video] = root.findAll({role: RoleType.BUTTON});
+  const root = await this.runWithLoadedTree(site);
+  const [audio, video] = root.findAll({role: RoleType.BUTTON});
 
-    assertNotNullNorUndefined(audio);
-    assertNotNullNorUndefined(video);
+  assertNotNullNorUndefined(audio);
+  assertNotNullNorUndefined(video);
 
-    assertEquals(undefined, audio.name);
-    assertEquals(undefined, video.name);
-    assertEquals(undefined, audio.firstChild);
-    assertEquals(undefined, video.firstChild);
+  assertEquals(undefined, audio.name);
+  assertEquals(undefined, video.name);
+  assertEquals(undefined, audio.firstChild);
+  assertEquals(undefined, video.firstChild);
 
-    // Fake the roles.
-    Object.defineProperty(audio, 'role', {
-      get() {
-        return chrome.automation.RoleType.AUDIO;
-      }
+  // Fake the roles.
+  Object.defineProperty(audio, 'role', {
+    get() {
+      return chrome.automation.RoleType.AUDIO;
+    }
+  });
+
+  Object.defineProperty(video, 'role', {
+    get() {
+      return chrome.automation.RoleType.VIDEO;
+    }
+  });
+
+  mockFeedback.call(doCmd('nextObject'))
+      .expectSpeech('Video')
+      .call(doCmd('previousObject'))
+      .expectSpeech('Audio')
+      .replay();
+});
+
+TEST_F('ChromeVoxBackgroundTest', 'AlertNoAnnouncement', async function() {
+  const mockFeedback = this.createMockFeedback();
+  const root = await this.runWithLoadedTree('<button></button>');
+  ChromeVoxState.addObserver(new class {
+    onCurrentRangeChanged(range) {
+      assertNotReached('Range was changed unexpectedly.');
+    }
+  }());
+  const button = root.find({role: RoleType.BUTTON});
+  const alertEvt = new CustomAutomationEvent(EventType.ALERT, button);
+  mockFeedback
+      .call(DesktopAutomationInterface.instance.onAlert.bind(
+          DesktopAutomationInterface.instance, alertEvt))
+      .call(() => assertFalse(mockFeedback.utteranceInQueue('Alert')))
+      .replay();
+});
+
+TEST_F('ChromeVoxBackgroundTest', 'AlertAnnouncement', async function() {
+  const mockFeedback = this.createMockFeedback();
+  const root = await this.runWithLoadedTree('<button>hello world</button>');
+  ChromeVoxState.addObserver(new class {
+    onCurrentRangeChanged(range) {
+      assertNotReached('Range was changed unexpectedly.');
+    }
+  }());
+
+  const button = root.find({role: RoleType.BUTTON});
+  const alertEvt = new CustomAutomationEvent(EventType.ALERT, button);
+  mockFeedback
+      .call(DesktopAutomationInterface.instance.onAlert.bind(
+          DesktopAutomationInterface.instance, alertEvt))
+      .expectNextSpeechUtteranceIsNot('Alert')
+      .expectSpeech('hello world')
+      .replay();
+});
+
+TEST_F(
+    'ChromeVoxBackgroundTest', 'SwipeLeftRight4ByContainers', async function() {
+      const mockFeedback = this.createMockFeedback();
+      const root = await this.runWithLoadedTree(`<p>test</p>`);
+      mockFeedback.call(doGesture(Gesture.SWIPE_RIGHT4))
+          .expectSpeech('Launcher', 'Button', 'Shelf', 'Tool bar', ', window')
+          .call(doGesture(Gesture.SWIPE_RIGHT4))
+          .expectSpeech('Shelf', 'Tool bar')
+          .call(doGesture(Gesture.SWIPE_RIGHT4))
+          .expectSpeech(/Status tray*/)
+          .call(doGesture(Gesture.SWIPE_RIGHT4))
+          .expectSpeech(/Address and search bar*/)
+
+          .call(doGesture(Gesture.SWIPE_LEFT4))
+          .expectSpeech(/Status tray*/)
+          .call(doGesture(Gesture.SWIPE_LEFT4))
+          .expectSpeech('Shelf', 'Tool bar')
+
+          .replay();
     });
 
-    Object.defineProperty(video, 'role', {
-      get() {
-        return chrome.automation.RoleType.VIDEO;
-      }
-    });
-
-    mockFeedback.call(doCmd('nextObject'))
-        .expectSpeech('Video')
-        .call(doCmd('previousObject'))
-        .expectSpeech('Audio')
-        .replay();
-  });
-});
-
-TEST_F('ChromeVoxBackgroundTest', 'AlertNoAnnouncement', function() {
-  const mockFeedback = this.createMockFeedback();
-  this.runWithLoadedTree('<button></button>', function(root) {
-    ChromeVoxState.addObserver(new class {
-      onCurrentRangeChanged(range) {
-        assertNotReached('Range was changed unexpectedly.');
-      }
-    }());
-    const button = root.find({role: RoleType.BUTTON});
-    const alertEvt = new CustomAutomationEvent(EventType.ALERT, button);
-    mockFeedback
-        .call(DesktopAutomationInterface.instance.onAlert.bind(
-            DesktopAutomationInterface.instance, alertEvt))
-        .call(() => assertFalse(mockFeedback.utteranceInQueue('Alert')))
-        .replay();
-  });
-});
-
-TEST_F('ChromeVoxBackgroundTest', 'AlertAnnouncement', function() {
-  const mockFeedback = this.createMockFeedback();
-  this.runWithLoadedTree('<button>hello world</button>', function(root) {
-    ChromeVoxState.addObserver(new class {
-      onCurrentRangeChanged(range) {
-        assertNotReached('Range was changed unexpectedly.');
-      }
-    }());
-
-    const button = root.find({role: RoleType.BUTTON});
-    const alertEvt = new CustomAutomationEvent(EventType.ALERT, button);
-    mockFeedback
-        .call(DesktopAutomationInterface.instance.onAlert.bind(
-            DesktopAutomationInterface.instance, alertEvt))
-        .expectNextSpeechUtteranceIsNot('Alert')
-        .expectSpeech('hello world')
-        .replay();
-  });
-});
-
-TEST_F('ChromeVoxBackgroundTest', 'SwipeLeftRight4ByContainers', function() {
-  const mockFeedback = this.createMockFeedback();
-  this.runWithLoadedTree(`<p>test</p>`, function(root) {
-    mockFeedback.call(doGesture(Gesture.SWIPE_RIGHT4))
-        .expectSpeech('Launcher', 'Button', 'Shelf', 'Tool bar', ', window')
-        .call(doGesture(Gesture.SWIPE_RIGHT4))
-        .expectSpeech('Shelf', 'Tool bar')
-        .call(doGesture(Gesture.SWIPE_RIGHT4))
-        .expectSpeech(/Status tray*/)
-        .call(doGesture(Gesture.SWIPE_RIGHT4))
-        .expectSpeech(/Address and search bar*/)
-
-        .call(doGesture(Gesture.SWIPE_LEFT4))
-        .expectSpeech(/Status tray*/)
-        .call(doGesture(Gesture.SWIPE_LEFT4))
-        .expectSpeech('Shelf', 'Tool bar')
-
-        .replay();
-  });
-});
-
-TEST_F('ChromeVoxBackgroundTest', 'SwipeLeftRight2', function() {
+TEST_F('ChromeVoxBackgroundTest', 'SwipeLeftRight2', async function() {
   const mockFeedback = this.createMockFeedback();
   const site = `
     <p id="live" aria-live="polite"</p>
@@ -3132,19 +3036,20 @@
     });
     </script>
   `;
-  this.runWithLoadedTree(site, function(root) {
-    mockFeedback.call(doGesture(Gesture.SWIPE_RIGHT2)).expectSpeech('Enter');
-    mockFeedback.call(doGesture(Gesture.SWIPE_LEFT2))
-        .expectSpeech('Escape')
-        .replay();
-  });
+  const root = await this.runWithLoadedTree(site);
+  mockFeedback.call(doGesture(Gesture.SWIPE_RIGHT2)).expectSpeech('Enter');
+  mockFeedback.call(doGesture(Gesture.SWIPE_LEFT2))
+      .expectSpeech('Escape')
+      .replay();
 });
 
 // TODO(crbug.com/1228418) - Improve the generation of summaries across ChromeOS
-TEST_F('ChromeVoxBackgroundTest', 'AlertDialogAutoSummaryTextContent', function() {
-  this.resetContextualOutput();
-  const mockFeedback = this.createMockFeedback();
-  const site = `
+TEST_F(
+    'ChromeVoxBackgroundTest', 'AlertDialogAutoSummaryTextContent',
+    async function() {
+      this.resetContextualOutput();
+      const mockFeedback = this.createMockFeedback();
+      const site = `
     <p>start</p>
     <div role="alertdialog" aria-label="Setup">
       <h1>Welcome</h1>
@@ -3154,118 +3059,115 @@
     </div>
     <p>end</p>
   `;
-  this.runWithLoadedTree(site, function(root) {
-    mockFeedback.call(doCmd('nextObject'))
-        .expectSpeech('Setup')
-        .expectSpeech(`Welcome This is some introductory text Exit Let's go`)
-        .expectSpeech('Welcome', 'Heading 1')
-        .call(doCmd('nextObject'))
-        .expectSpeech('This is some introductory text')
-        .call(doCmd('nextObject'))
-        .expectSpeech('Exit', 'Button')
-        .call(doCmd('nextObject'))
-        .expectSpeech(`Let's go`, 'Button')
-        .call(doCmd('nextObject'))
-        .expectSpeech('end')
+      const root = await this.runWithLoadedTree(site);
+      mockFeedback.call(doCmd('nextObject'))
+          .expectSpeech('Setup')
+          .expectSpeech(`Welcome This is some introductory text Exit Let's go`)
+          .expectSpeech('Welcome', 'Heading 1')
+          .call(doCmd('nextObject'))
+          .expectSpeech('This is some introductory text')
+          .call(doCmd('nextObject'))
+          .expectSpeech('Exit', 'Button')
+          .call(doCmd('nextObject'))
+          .expectSpeech(`Let's go`, 'Button')
+          .call(doCmd('nextObject'))
+          .expectSpeech('end')
 
-        .call(doCmd('previousObject'))
-        .expectSpeech(`Let's go`, 'Button')
-        .expectSpeech('Setup')
-        .expectSpeech(`Welcome This is some introductory text Exit Let's go`)
+          .call(doCmd('previousObject'))
+          .expectSpeech(`Let's go`, 'Button')
+          .expectSpeech('Setup')
+          .expectSpeech(`Welcome This is some introductory text Exit Let's go`)
 
-        .replay();
-  });
-});
+          .replay();
+    });
 
-TEST_F('ChromeVoxBackgroundTest', 'ImageAnnotations', function() {
+TEST_F('ChromeVoxBackgroundTest', 'ImageAnnotations', async function() {
   const mockFeedback = this.createMockFeedback();
   const site = `
     <p>start</p>
     <img alt="bar" src="data:image/png;base64,iVBORw0KGgoAAAANS">
     <img src="data:image/png;base64,iVBORw0KGgoAAAANS">
   `;
-  this.runWithLoadedTree(site, function(root) {
-    const [namedImg, unnamedImg] = root.findAll({role: RoleType.IMAGE});
+  const root = await this.runWithLoadedTree(site);
+  const [namedImg, unnamedImg] = root.findAll({role: RoleType.IMAGE});
 
-    assertNotNullNorUndefined(namedImg);
-    assertNotNullNorUndefined(unnamedImg);
+  assertNotNullNorUndefined(namedImg);
+  assertNotNullNorUndefined(unnamedImg);
 
-    assertEquals('bar', namedImg.name);
-    assertEquals(undefined, unnamedImg.name);
+  assertEquals('bar', namedImg.name);
+  assertEquals(undefined, unnamedImg.name);
 
-    // Fake the image annotation.
-    Object.defineProperty(namedImg, 'imageAnnotation', {
-      get() {
-        return 'foo';
-      }
-    });
-    Object.defineProperty(unnamedImg, 'imageAnnotation', {
-      get() {
-        return 'foo';
-      }
-    });
-
-    mockFeedback.call(doCmd('nextObject'))
-        .expectSpeech('start')
-        .expectNextSpeechUtteranceIsNot('foo')
-        .expectSpeech('bar', 'Image')
-        .call(doCmd('nextObject'))
-        .expectNextSpeechUtteranceIsNot('bar')
-        .expectSpeech('foo', 'Image')
-        .replay();
+  // Fake the image annotation.
+  Object.defineProperty(namedImg, 'imageAnnotation', {
+    get() {
+      return 'foo';
+    }
   });
+  Object.defineProperty(unnamedImg, 'imageAnnotation', {
+    get() {
+      return 'foo';
+    }
+  });
+
+  mockFeedback.call(doCmd('nextObject'))
+      .expectSpeech('start')
+      .expectNextSpeechUtteranceIsNot('foo')
+      .expectSpeech('bar', 'Image')
+      .call(doCmd('nextObject'))
+      .expectNextSpeechUtteranceIsNot('bar')
+      .expectSpeech('foo', 'Image')
+      .replay();
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'VolumeChanges', function() {
+TEST_F('ChromeVoxBackgroundTest', 'VolumeChanges', async function() {
   const mockFeedback = this.createMockFeedback();
-  this.runWithLoadedTree('<p>test</p>', function() {
-    const bounds = ChromeVoxState.instance.getFocusBounds();
-    mockFeedback.call(press(KeyCode.VOLUME_UP))
-        .expectSpeech('Volume', 'Slider', /\d+%/)
-        .call(() => {
-          // The bounds should not have changed.
-          assertEquals(
-              JSON.stringify(bounds),
-              JSON.stringify(ChromeVoxState.instance.getFocusBounds()));
-        })
-        .replay();
-  });
+  await this.runWithLoadedTree('<p>test</p>');
+  const bounds = ChromeVoxState.instance.getFocusBounds();
+  mockFeedback.call(press(KeyCode.VOLUME_UP))
+      .expectSpeech('Volume', 'Slider', /\d+%/)
+      .call(() => {
+        // The bounds should not have changed.
+        assertEquals(
+            JSON.stringify(bounds),
+            JSON.stringify(ChromeVoxState.instance.getFocusBounds()));
+      })
+      .replay();
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'WrapContentEditableAtEndOfDoc', function() {
-  const mockFeedback = this.createMockFeedback();
-  const site = `<p>start</p>
+TEST_F(
+    'ChromeVoxBackgroundTest', 'WrapContentEditableAtEndOfDoc',
+    async function() {
+      const mockFeedback = this.createMockFeedback();
+      const site = `<p>start</p>
       <div role="textbox" contenteditable aria-multiline="false"></div>`;
-  this.runWithLoadedTree(site, function() {
-    mockFeedback.call(doCmd('nextObject'))
-        .expectSpeech('Edit text')
-        .call(doCmd('nextObject'))
-        .expectEarcon(Earcon.WRAP)
-        .expectSpeech('Web Content')
-        .call(doCmd('nextObject'))
-        .expectSpeech('start')
-        .replay();
-  });
-});
+      await this.runWithLoadedTree(site);
+      mockFeedback.call(doCmd('nextObject'))
+          .expectSpeech('Edit text')
+          .call(doCmd('nextObject'))
+          .expectEarcon(Earcon.WRAP)
+          .expectSpeech('Web Content')
+          .call(doCmd('nextObject'))
+          .expectSpeech('start')
+          .replay();
+    });
 
-TEST_F('ChromeVoxBackgroundTest', 'ReadFromHereBlankNodes', function() {
+TEST_F('ChromeVoxBackgroundTest', 'ReadFromHereBlankNodes', async function() {
   const mockFeedback = this.createMockFeedback();
   const site = `<a tabindex=0></a><p>start</p><a tabindex=0></a><p>end</p>`;
-  this.runWithLoadedTree(site, function(root) {
-    assertEquals(
-        RoleType.STATIC_TEXT,
-        ChromeVoxState.instance.currentRange.start.node.role);
+  const root = await this.runWithLoadedTree(site);
+  assertEquals(
+      RoleType.STATIC_TEXT,
+      ChromeVoxState.instance.currentRange.start.node.role);
 
-    // "start" is uttered twice, once for the initial focus as the page loads,
-    // and once during the 'read from here' command.
-    mockFeedback.expectSpeech('start')
-        .call(doCmd('readFromHere'))
-        .expectSpeech('start', 'end')
-        .replay();
-  });
+  // "start" is uttered twice, once for the initial focus as the page loads,
+  // and once during the 'read from here' command.
+  mockFeedback.expectSpeech('start')
+      .call(doCmd('readFromHere'))
+      .expectSpeech('start', 'end')
+      .replay();
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'ContainerButtons', function() {
+TEST_F('ChromeVoxBackgroundTest', 'ContainerButtons', async function() {
   const mockFeedback = this.createMockFeedback();
 
   // This pattern can be found in ARC++/YouTube.
@@ -3275,25 +3177,25 @@
       <div role="group">4 minutes, Cat Video</div>
     </div>
   `;
-  this.runWithLoadedTree(site, function(root) {
-    const group = root.find({role: RoleType.GROUP});
+  const root = await this.runWithLoadedTree(site);
+  const group = root.find({role: RoleType.GROUP});
 
-    Object.defineProperty(group, 'clickable', {
-      get() {
-        return true;
-      }
-    });
-
-    mockFeedback.call(doCmd('nextObject'))
-        .expectSpeech('Cat Video', 'Button')
-        .call(doCmd('nextObject'))
-        .expectSpeech('4 minutes, Cat Video')
-        .replay();
+  Object.defineProperty(group, 'clickable', {
+    get() {
+      return true;
+    }
   });
+
+  mockFeedback.call(doCmd('nextObject'))
+      .expectSpeech('Cat Video', 'Button')
+      .call(doCmd('nextObject'))
+      .expectSpeech('4 minutes, Cat Video')
+      .replay();
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'FocusOnWebAreaIgnoresEvents', function() {
-  const site = `
+TEST_F(
+    'ChromeVoxBackgroundTest', 'FocusOnWebAreaIgnoresEvents', async function() {
+      const site = `
     <div role="application" tabindex=0 aria-label="container">
       <select>
         <option>apple</option>
@@ -3311,45 +3213,45 @@
           });
     </script>
   `;
-  this.runWithLoadedTree(site, async function(root) {
-    const application = root.find({role: RoleType.APPLICATION});
-    const popUpButton = root.find({role: RoleType.POP_UP_BUTTON});
-    const p = root.find({role: RoleType.PARAGRAPH});
+      const root = await this.runWithLoadedTree(site);
+      const application = root.find({role: RoleType.APPLICATION});
+      const popUpButton = root.find({role: RoleType.POP_UP_BUTTON});
+      const p = root.find({role: RoleType.PARAGRAPH});
 
-    // Move focus to the select, which honors value changes through
-    // FocusAutomationHandler.
-    popUpButton.focus();
-    await TestUtils.waitForSpeech('apple');
+      // Move focus to the select, which honors value changes through
+      // FocusAutomationHandler.
+      popUpButton.focus();
+      await TestUtils.waitForSpeech('apple');
 
-    // Clicking the paragraph programmatically changes the select value.
-    p.doDefault();
-    await TestUtils.waitForSpeech('grape');
-    assertEquals(
-        RoleType.POP_UP_BUTTON,
-        ChromeVoxState.instance.currentRange.start.node.role);
+      // Clicking the paragraph programmatically changes the select value.
+      p.doDefault();
+      await TestUtils.waitForSpeech('grape');
+      assertEquals(
+          RoleType.POP_UP_BUTTON,
+          ChromeVoxState.instance.currentRange.start.node.role);
 
-    // Now, move focus to the application which is a parent of the select.
-    application.focus();
-    await TestUtils.waitForSpeech('container');
+      // Now, move focus to the application which is a parent of the select.
+      application.focus();
+      await TestUtils.waitForSpeech('container');
 
-    // Hook into the speak call, to see what comes next.
-    let nextSpeech;
-    ChromeVox.tts.speak = textString => {
-      nextSpeech = textString;
-    };
+      // Hook into the speak call, to see what comes next.
+      let nextSpeech;
+      ChromeVox.tts.speak = textString => {
+        nextSpeech = textString;
+      };
 
-    // Trigger another value update for the select.
-    p.doDefault();
+      // Trigger another value update for the select.
+      p.doDefault();
 
-    // This comes when the <select>'s value changes.
-    await TestUtils.waitForEvent(application, EventType.SELECTED_VALUE_CHANGED);
+      // This comes when the <select>'s value changes.
+      await TestUtils.waitForEvent(
+          application, EventType.SELECTED_VALUE_CHANGED);
 
-    // Nothing should have been spoken.
-    assertEquals(undefined, nextSpeech);
-  });
-});
+      // Nothing should have been spoken.
+      assertEquals(undefined, nextSpeech);
+    });
 
-TEST_F('ChromeVoxBackgroundTest', 'AriaLeaves', function() {
+TEST_F('ChromeVoxBackgroundTest', 'AriaLeaves', async function() {
   const mockFeedback = this.createMockFeedback();
   const site = `
     <div role="radio"><p>PM</p></div>
@@ -3360,34 +3262,33 @@
       p.addEventListener('click', () => {});
     </script>
   `;
-  this.runWithLoadedTree(site, function(root) {
-    mockFeedback.expectSpeech('PM, radio button unselected')
-        .call(doCmd('nextObject'))
-        .expectSpeech('PM')
-        .call(
-            () => assertEquals(
-                RoleType.STATIC_TEXT,
-                ChromeVoxState.instance.currentRange.start.node.role))
+  const root = await this.runWithLoadedTree(site);
+  mockFeedback.expectSpeech('PM, radio button unselected')
+      .call(doCmd('nextObject'))
+      .expectSpeech('PM')
+      .call(
+          () => assertEquals(
+              RoleType.STATIC_TEXT,
+              ChromeVoxState.instance.currentRange.start.node.role))
 
-        .call(doCmd('nextObject'))
-        .expectSpeech('Agree, switch off')
-        .call(
-            () => assertEquals(
-                RoleType.SWITCH,
-                ChromeVoxState.instance.currentRange.start.node.role))
+      .call(doCmd('nextObject'))
+      .expectSpeech('Agree, switch off')
+      .call(
+          () => assertEquals(
+              RoleType.SWITCH,
+              ChromeVoxState.instance.currentRange.start.node.role))
 
-        .call(doCmd('nextObject'))
-        .expectSpeech('Agree', 'Check box')
-        .call(
-            () => assertEquals(
-                RoleType.CHECK_BOX,
-                ChromeVoxState.instance.currentRange.start.node.role))
+      .call(doCmd('nextObject'))
+      .expectSpeech('Agree', 'Check box')
+      .call(
+          () => assertEquals(
+              RoleType.CHECK_BOX,
+              ChromeVoxState.instance.currentRange.start.node.role))
 
-        .replay();
-  });
+      .replay();
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'MarkedContent', function() {
+TEST_F('ChromeVoxBackgroundTest', 'MarkedContent', async function() {
   this.resetContextualOutput();
   const mockFeedback = this.createMockFeedback();
   const site = `
@@ -3403,48 +3304,49 @@
     <span>This is </span><span role="suggestion"><span
         role="deletion">everyone's</span></span><span> text.</span>
   `;
-  this.runWithLoadedTree(site, function(rootNode) {
-    mockFeedback.expectSpeech('Start')
-        .call(doCmd('nextObject'))
-        .expectSpeech('This is ')
-        .call(doCmd('nextObject'))
-        .expectSpeech('Marked content', 'my', 'Marked content end')
-        .expectBraille('Marked content my Marked content end')
-        .call(doCmd('nextObject'))
-        .expectSpeech(' text.')
-        .expectBraille(' text.')
-        .call(doCmd('nextObject'))
-        .expectSpeech('This is ')
-        .call(doCmd('nextObject'))
-        .expectSpeech('Comment', 'your', 'Comment end')
-        .expectBraille('Comment your Comment end')
-        .call(doCmd('nextObject'))
-        .expectSpeech(' text.')
-        .expectBraille(' text.')
-        .call(doCmd('nextObject'))
-        .expectSpeech('This is ')
-        .call(doCmd('nextObject'))
-        .expectSpeech('Suggest', 'Insert', 'their', 'Insert end', 'Suggest end')
-        .expectBraille('Suggest Insert their Insert end Suggest end')
-        .call(doCmd('nextObject'))
-        .expectSpeech(' text.')
-        .expectBraille(' text.')
-        .call(doCmd('nextObject'))
-        .expectSpeech('This is ')
-        .call(doCmd('nextObject'))
-        .expectSpeech(
-            'Suggest', 'Delete', `everyone's`, 'Delete end', 'Suggest end')
-        .expectBraille(`Suggest Delete everyone's Delete end Suggest end`)
-        .call(doCmd('nextObject'))
-        .expectSpeech(' text.')
-        .expectBraille(' text.')
-        .replay();
-  });
+  const rootNode = await this.runWithLoadedTree(site);
+  mockFeedback.expectSpeech('Start')
+      .call(doCmd('nextObject'))
+      .expectSpeech('This is ')
+      .call(doCmd('nextObject'))
+      .expectSpeech('Marked content', 'my', 'Marked content end')
+      .expectBraille('Marked content my Marked content end')
+      .call(doCmd('nextObject'))
+      .expectSpeech(' text.')
+      .expectBraille(' text.')
+      .call(doCmd('nextObject'))
+      .expectSpeech('This is ')
+      .call(doCmd('nextObject'))
+      .expectSpeech('Comment', 'your', 'Comment end')
+      .expectBraille('Comment your Comment end')
+      .call(doCmd('nextObject'))
+      .expectSpeech(' text.')
+      .expectBraille(' text.')
+      .call(doCmd('nextObject'))
+      .expectSpeech('This is ')
+      .call(doCmd('nextObject'))
+      .expectSpeech('Suggest', 'Insert', 'their', 'Insert end', 'Suggest end')
+      .expectBraille('Suggest Insert their Insert end Suggest end')
+      .call(doCmd('nextObject'))
+      .expectSpeech(' text.')
+      .expectBraille(' text.')
+      .call(doCmd('nextObject'))
+      .expectSpeech('This is ')
+      .call(doCmd('nextObject'))
+      .expectSpeech(
+          'Suggest', 'Delete', `everyone's`, 'Delete end', 'Suggest end')
+      .expectBraille(`Suggest Delete everyone's Delete end Suggest end`)
+      .call(doCmd('nextObject'))
+      .expectSpeech(' text.')
+      .expectBraille(' text.')
+      .replay();
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'ClickAncestorAreNotActionable', function() {
-  const mockFeedback = this.createMockFeedback();
-  const site = `
+TEST_F(
+    'ChromeVoxBackgroundTest', 'ClickAncestorAreNotActionable',
+    async function() {
+      const mockFeedback = this.createMockFeedback();
+      const site = `
     <p>Start</p>
     <div id="button1" role="button" aria-label="OK">
       <div role="group">OK</div>
@@ -3458,63 +3360,62 @@
       document.getElementById('button2').addEventListener('click', () => {});
     </script>
   `;
-  this.runWithLoadedTree(site, function(rootNode) {
-    mockFeedback.expectSpeech('Start')
-        .call(doCmd('nextObject'))
-        .expectSpeech('OK')
-        .call(doCmd('nextObject'))
-        .expectSpeech('cancel')
-        .call(doCmd('nextObject'))
-        .expectSpeech('more info')
-        .call(doCmd('nextObject'))
-        .expectSpeech('end')
-        .replay();
-  });
-});
+      const rootNode = await this.runWithLoadedTree(site);
+      mockFeedback.expectSpeech('Start')
+          .call(doCmd('nextObject'))
+          .expectSpeech('OK')
+          .call(doCmd('nextObject'))
+          .expectSpeech('cancel')
+          .call(doCmd('nextObject'))
+          .expectSpeech('more info')
+          .call(doCmd('nextObject'))
+          .expectSpeech('end')
+          .replay();
+    });
 
-TEST_F('ChromeVoxBackgroundTest', 'TouchEditingState', function() {
+TEST_F('ChromeVoxBackgroundTest', 'TouchEditingState', async function() {
   const mockFeedback = this.createMockFeedback();
   const site = `
     <p>Start</p>
     <input type="text"></input>
   `;
-  this.runWithLoadedTree(site, function(rootNode) {
-    const bounds = rootNode.find({role: RoleType.TEXT_FIELD}).location;
-    mockFeedback.expectSpeech('Start')
-        .call(doGesture(
-            chrome.accessibilityPrivate.Gesture.TOUCH_EXPLORE, bounds.left,
-            bounds.top))
-        .expectSpeech('Edit text', 'Double tap to start editing')
-        .call(doGesture(
-            chrome.accessibilityPrivate.Gesture.CLICK, bounds.left, bounds.top))
-        .expectSpeech('Edit text', 'is editing')
-        .replay();
-  });
+  const rootNode = await this.runWithLoadedTree(site);
+  const bounds = rootNode.find({role: RoleType.TEXT_FIELD}).location;
+  mockFeedback.expectSpeech('Start')
+      .call(doGesture(
+          chrome.accessibilityPrivate.Gesture.TOUCH_EXPLORE, bounds.left,
+          bounds.top))
+      .expectSpeech('Edit text', 'Double tap to start editing')
+      .call(doGesture(
+          chrome.accessibilityPrivate.Gesture.CLICK, bounds.left, bounds.top))
+      .expectSpeech('Edit text', 'is editing')
+      .replay();
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'TouchGesturesProducesEarcons', function() {
-  const mockFeedback = this.createMockFeedback();
-  const site = `
+TEST_F(
+    'ChromeVoxBackgroundTest', 'TouchGesturesProducesEarcons',
+    async function() {
+      const mockFeedback = this.createMockFeedback();
+      const site = `
     <p>Start</p>
     <button>ok</button>
     <a href="chromevox.com">cancel</a>
   `;
-  this.runWithLoadedTree(site, function(rootNode) {
-    mockFeedback.expectSpeech('Start')
-        .call(doGesture(chrome.accessibilityPrivate.Gesture.SWIPE_RIGHT1))
-        .expectSpeech('ok', 'Button')
-        .expectEarcon(Earcon.BUTTON)
-        .call(doGesture(chrome.accessibilityPrivate.Gesture.SWIPE_RIGHT1))
-        .expectSpeech('cancel', 'Link')
-        .expectEarcon(Earcon.LINK)
-        .call(doGesture(chrome.accessibilityPrivate.Gesture.SWIPE_LEFT1))
-        .expectSpeech('ok', 'Button')
-        .expectEarcon(Earcon.BUTTON)
-        .replay();
-  });
-});
+      const rootNode = await this.runWithLoadedTree(site);
+      mockFeedback.expectSpeech('Start')
+          .call(doGesture(chrome.accessibilityPrivate.Gesture.SWIPE_RIGHT1))
+          .expectSpeech('ok', 'Button')
+          .expectEarcon(Earcon.BUTTON)
+          .call(doGesture(chrome.accessibilityPrivate.Gesture.SWIPE_RIGHT1))
+          .expectSpeech('cancel', 'Link')
+          .expectEarcon(Earcon.LINK)
+          .call(doGesture(chrome.accessibilityPrivate.Gesture.SWIPE_LEFT1))
+          .expectSpeech('ok', 'Button')
+          .expectEarcon(Earcon.BUTTON)
+          .replay();
+    });
 
-TEST_F('ChromeVoxBackgroundTest', 'Separator', function() {
+TEST_F('ChromeVoxBackgroundTest', 'Separator', async function() {
   const mockFeedback = this.createMockFeedback();
   const site = `
     <p>Start</p>
@@ -3522,20 +3423,19 @@
     <p><span role="separator">Separator content should be read</span></p>
     <p><span>World</span></p>
   `;
-  this.runWithLoadedTree(site, function(rootNode) {
-    mockFeedback.expectSpeech('Start')
-        .call(doCmd('nextObject'))
-        .expectSpeech('Hello')
-        .call(doCmd('nextObject'))
-        .expectSpeech('Separator content should be read', 'Separator')
-        .expectBraille('Separator content should be read seprtr')
-        .call(doCmd('nextObject'))
-        .expectSpeech('World')
-        .replay();
-  });
+  const rootNode = await this.runWithLoadedTree(site);
+  mockFeedback.expectSpeech('Start')
+      .call(doCmd('nextObject'))
+      .expectSpeech('Hello')
+      .call(doCmd('nextObject'))
+      .expectSpeech('Separator content should be read', 'Separator')
+      .expectBraille('Separator content should be read seprtr')
+      .call(doCmd('nextObject'))
+      .expectSpeech('World')
+      .replay();
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'FocusAfterClick', function() {
+TEST_F('ChromeVoxBackgroundTest', 'FocusAfterClick', async function() {
   const mockFeedback = this.createMockFeedback();
   const site = `
     <p>Start</p>
@@ -3547,20 +3447,19 @@
       }, false);
     </script>
   `;
-  this.runWithLoadedTree(site, function(root) {
-    BaseAutomationHandler.announceActions = false;
-    mockFeedback.expectSpeech('Start')
-        .call(doCmd('nextObject'))
-        .expectSpeech('Click me')
-        .call(doCmd('forceClickOnCurrentItem'))
-        .expectSpeech('Focus me')
-        .call(() => {
-          assertEquals(
-              'Focus me',
-              ChromeVoxState.instance.getCurrentRange().start.node.name);
-        })
-        .replay();
-  });
+  const root = await this.runWithLoadedTree(site);
+  BaseAutomationHandler.announceActions = false;
+  mockFeedback.expectSpeech('Start')
+      .call(doCmd('nextObject'))
+      .expectSpeech('Click me')
+      .call(doCmd('forceClickOnCurrentItem'))
+      .expectSpeech('Focus me')
+      .call(() => {
+        assertEquals(
+            'Focus me',
+            ChromeVoxState.instance.getCurrentRange().start.node.name);
+      })
+      .replay();
 });
 
 SYNC_TEST_F('ChromeVoxBackgroundTest', 'EarconPlayback', function() {
@@ -3605,74 +3504,76 @@
   assertEquals(0, Object.keys(engine.lastEarconSources_).length);
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'MixedNavWithRangeInvalidation', function() {
-  const mockFeedback = this.createMockFeedback();
-  const site = `
+TEST_F(
+    'ChromeVoxBackgroundTest', 'MixedNavWithRangeInvalidation',
+    async function() {
+      const mockFeedback = this.createMockFeedback();
+      const site = `
     <p>Start</p>
     <button>apple</button>
     <a href="google.com">grape</a>
     <button>banana</button>
   `;
-  this.runWithLoadedTree(site, function(root) {
-    // Different ways to navigate to the next object.
-    const keyboardHandler = ChromeVoxState.instance.keyboardHandler_;
-    const nextObjectKeyboard = keyboardHandler.onKeyDown.bind(keyboardHandler, {
-      keyCode: KeyCode.RIGHT,
-      metaKey: true,
-      preventDefault: () => {},
-      stopPropagation: () => {}
+      const root = await this.runWithLoadedTree(site);
+      // Different ways to navigate to the next object.
+      const keyboardHandler = ChromeVoxState.instance.keyboardHandler_;
+      const nextObjectKeyboard =
+          keyboardHandler.onKeyDown.bind(keyboardHandler, {
+            keyCode: KeyCode.RIGHT,
+            metaKey: true,
+            preventDefault: () => {},
+            stopPropagation: () => {}
+          });
+      const nextObjectBraille = BrailleCommandHandler.onBrailleKeyEvent.bind(
+          BrailleCommandHandler, {command: BrailleKeyCommand.PAN_RIGHT});
+      const nextObjectGesture =
+          GestureCommandHandler.onAccessibilityGesture_.bind(
+              GestureCommandHandler, Gesture.SWIPE_RIGHT1);
+      const clearCurrentRange = ChromeVoxState.instance.setCurrentRange.bind(
+          ChromeVoxState.instance, null);
+      const toggleTalkBack = () => {
+        ChromeVoxState.instance.talkBackEnabled =
+            !ChromeVoxState.instance.talkBackEnabled;
+      };
+
+      mockFeedback
+          .expectSpeech('Start')
+
+          .call(clearCurrentRange)
+          .call(nextObjectKeyboard)
+          .expectSpeech('apple', 'Button')
+
+          .call(clearCurrentRange)
+          .call(nextObjectBraille)
+          .expectSpeech('grape', 'Link')
+
+          .call(clearCurrentRange)
+          .call(nextObjectGesture)
+          .expectSpeech('banana', 'Button')
+
+          .call(clearCurrentRange)
+          .call(nextObjectKeyboard)
+          .expectSpeech('Web Content')
+
+          .call(clearCurrentRange)
+          .call(toggleTalkBack)
+          .call(nextObjectKeyboard)
+          .call(() => assertFalse(!!ChromeVoxState.instance.currentRange))
+
+          .call(nextObjectBraille)
+          .call(() => assertFalse(!!ChromeVoxState.instance.currentRange))
+
+          .call(nextObjectGesture)
+          .call(() => assertFalse(!!ChromeVoxState.instance.currentRange))
+
+          .call(toggleTalkBack)
+          .call(nextObjectKeyboard)
+          .call(() => assertTrue(!!ChromeVoxState.instance.currentRange))
+
+          .replay();
     });
-    const nextObjectBraille = BrailleCommandHandler.onBrailleKeyEvent.bind(
-        BrailleCommandHandler, {command: BrailleKeyCommand.PAN_RIGHT});
-    const nextObjectGesture =
-        GestureCommandHandler.onAccessibilityGesture_.bind(
-            GestureCommandHandler, Gesture.SWIPE_RIGHT1);
-    const clearCurrentRange = ChromeVoxState.instance.setCurrentRange.bind(
-        ChromeVoxState.instance, null);
-    const toggleTalkBack = () => {
-      ChromeVoxState.instance.talkBackEnabled =
-          !ChromeVoxState.instance.talkBackEnabled;
-    };
 
-    mockFeedback
-        .expectSpeech('Start')
-
-        .call(clearCurrentRange)
-        .call(nextObjectKeyboard)
-        .expectSpeech('apple', 'Button')
-
-        .call(clearCurrentRange)
-        .call(nextObjectBraille)
-        .expectSpeech('grape', 'Link')
-
-        .call(clearCurrentRange)
-        .call(nextObjectGesture)
-        .expectSpeech('banana', 'Button')
-
-        .call(clearCurrentRange)
-        .call(nextObjectKeyboard)
-        .expectSpeech('Web Content')
-
-        .call(clearCurrentRange)
-        .call(toggleTalkBack)
-        .call(nextObjectKeyboard)
-        .call(() => assertFalse(!!ChromeVoxState.instance.currentRange))
-
-        .call(nextObjectBraille)
-        .call(() => assertFalse(!!ChromeVoxState.instance.currentRange))
-
-        .call(nextObjectGesture)
-        .call(() => assertFalse(!!ChromeVoxState.instance.currentRange))
-
-        .call(toggleTalkBack)
-        .call(nextObjectKeyboard)
-        .call(() => assertTrue(!!ChromeVoxState.instance.currentRange))
-
-        .replay();
-  });
-});
-
-TEST_F('ChromeVoxBackgroundTest', 'DetailsChanged', function() {
+TEST_F('ChromeVoxBackgroundTest', 'DetailsChanged', async function() {
   const mockFeedback = this.createMockFeedback();
 
   // Make sure we're not testing reading of the hint from the button's output
@@ -3688,24 +3589,20 @@
       });
     </script>
   `;
-  this.runWithLoadedTree(site, function(root) {
-    const button = root.find({role: RoleType.BUTTON});
-    mockFeedback.expectSpeech('ok')
-        .call(button.doDefault.bind(button))
-        .expectSpeech('Press Search+A, J to jump to details')
-        .replay();
-  });
+  const root = await this.runWithLoadedTree(site);
+  const button = root.find({role: RoleType.BUTTON});
+  mockFeedback.expectSpeech('ok')
+      .call(button.doDefault.bind(button))
+      .expectSpeech('Press Search+A, J to jump to details')
+      .replay();
 });
 
-SYNC_TEST_F('ChromeVoxBackgroundTest', 'PageLoadEarcons', async function() {
+SYNC_TEST_F('ChromeVoxBackgroundTest', 'PageLoadEarcons', function() {
   const sawEarcons = [];
   const fakeEarcons = {playEarcon: (earcon) => sawEarcons.push(earcon)};
   Object.defineProperty(ChromeVox, 'earcons', {get: () => fakeEarcons});
   AutomationUtil.getTopLevelRoot = (node) => node;
 
-  const module = await import('./page_load_sound_handler.js');
-  const PageLoadSoundHandler = module.PageLoadSoundHandler;
-
   // Use this specific object to control the load environment.
   const handler = new PageLoadSoundHandler();
 
@@ -3735,19 +3632,18 @@
       [Earcon.PAGE_START_LOADING, Earcon.PAGE_FINISH_LOADING], sawEarcons);
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'NewTabRead', function() {
+TEST_F('ChromeVoxBackgroundTest', 'NewTabRead', async function() {
   const mockFeedback = this.createMockFeedback();
   const site = `<p>start</p><p>end</p>`;
-  this.runWithLoadedTree(site, function(root) {
-    mockFeedback.call(doCmd('nextObject'))
-        .expectSpeech('end')
-        .call(press(KeyCode.T, {ctrl: true}))
-        .expectSpeech(/New Tab/)
-        .replay();
-  });
+  const root = await this.runWithLoadedTree(site);
+  mockFeedback.call(doCmd('nextObject'))
+      .expectSpeech('end')
+      .call(press(KeyCode.T, {ctrl: true}))
+      .expectSpeech(/New Tab/)
+      .replay();
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'NestedMenuHints', function() {
+TEST_F('ChromeVoxBackgroundTest', 'NestedMenuHints', async function() {
   const mockFeedback = this.createMockFeedback();
   const site = `
     <div role="menu" aria-orientation="vertical">
@@ -3757,18 +3653,16 @@
       </div>
     </div>
   `;
-  this.runWithLoadedTree(site, function(root) {
-    mockFeedback
-        .expectSpeech(
-            'Press left or right arrow to navigate; enter to activate')
-        .call(
-            () => assertFalse(mockFeedback.utteranceInQueue(
-                'Press up or down arrow to navigate; enter to activate')))
-        .replay();
-  });
+  const root = await this.runWithLoadedTree(site);
+  mockFeedback
+      .expectSpeech('Press left or right arrow to navigate; enter to activate')
+      .call(
+          () => assertFalse(mockFeedback.utteranceInQueue(
+              'Press up or down arrow to navigate; enter to activate')))
+      .replay();
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'SkipLabelDescriptionFor', function() {
+TEST_F('ChromeVoxBackgroundTest', 'SkipLabelDescriptionFor', async function() {
   const mockFeedback = this.createMockFeedback();
   const site = `
     <p>start</p>
@@ -3778,89 +3672,86 @@
     </label>
     <p>end</p>
   `;
-  this.runWithLoadedTree(site, function(root) {
-    mockFeedback.expectSpeech('start')
-        .call(doCmd('nextObject'))
-        .expectSpeech('Enable speech logging', 'Check box')
-        .call(doCmd('nextObject'))
-        .expectSpeech('end')
-        .call(doCmd('previousObject'))
-        .expectSpeech('Enable speech logging', 'Check box')
-        .call(doCmd('previousObject'))
-        .expectSpeech('start')
-        .replay();
-  });
+  const root = await this.runWithLoadedTree(site);
+  mockFeedback.expectSpeech('start')
+      .call(doCmd('nextObject'))
+      .expectSpeech('Enable speech logging', 'Check box')
+      .call(doCmd('nextObject'))
+      .expectSpeech('end')
+      .call(doCmd('previousObject'))
+      .expectSpeech('Enable speech logging', 'Check box')
+      .call(doCmd('previousObject'))
+      .expectSpeech('start')
+      .replay();
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'Abbreviation', function() {
+TEST_F('ChromeVoxBackgroundTest', 'Abbreviation', async function() {
   const mockFeedback = this.createMockFeedback();
   const site = `
     <abbr title="uniform resource locator">URL</abbr>
   `;
-  this.runWithLoadedTree(site, function(root) {
-    mockFeedback.expectSpeech('URL', 'uniform resource locator', 'Abbreviation')
-        .replay();
-  });
+  await this.runWithLoadedTree(site);
+  mockFeedback.expectSpeech('URL', 'uniform resource locator', 'Abbreviation')
+      .replay();
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'EndOfText', function() {
+TEST_F('ChromeVoxBackgroundTest', 'EndOfText', async function() {
   const mockFeedback = this.createMockFeedback();
   const site = `
     <p>start</p>
     <div tabindex=0 role="textbox" contenteditable><p>abc</p><p>123</p></div>
   `;
-  this.runWithLoadedTree(site, function(root) {
-    const contentEditable = root.find({role: RoleType.TEXT_FIELD});
+  const root = await this.runWithLoadedTree(site);
+  const contentEditable = root.find({role: RoleType.TEXT_FIELD});
 
-    this.listenOnce(contentEditable, EventType.FOCUS, function() {
-      mockFeedback.call(press(KeyCode.RIGHT))
-          .expectSpeech('b')
-          .call(press(KeyCode.RIGHT))
-          .expectSpeech('c')
-          .call(press(KeyCode.RIGHT))
-          .expectSpeech('\n')
-          .call(press(KeyCode.RIGHT))
-          .expectSpeech('1')
-          .call(press(KeyCode.RIGHT))
-          .expectSpeech('2')
-          .call(press(KeyCode.RIGHT))
-          .expectSpeech('3')
-          .call(press(KeyCode.RIGHT))
-          .expectSpeech('End of text')
+  this.listenOnce(contentEditable, EventType.FOCUS, function() {
+    mockFeedback.call(press(KeyCode.RIGHT))
+        .expectSpeech('b')
+        .call(press(KeyCode.RIGHT))
+        .expectSpeech('c')
+        .call(press(KeyCode.RIGHT))
+        .expectSpeech('\n')
+        .call(press(KeyCode.RIGHT))
+        .expectSpeech('1')
+        .call(press(KeyCode.RIGHT))
+        .expectSpeech('2')
+        .call(press(KeyCode.RIGHT))
+        .expectSpeech('3')
+        .call(press(KeyCode.RIGHT))
+        .expectSpeech('End of text')
 
-          .call(press(KeyCode.LEFT))
-          .expectSpeech('3')
-          .call(press(KeyCode.LEFT))
-          .expectSpeech('2')
-          .call(press(KeyCode.LEFT))
-          .expectSpeech('1')
-          .call(press(KeyCode.LEFT))
-          .expectSpeech('\n')
-          .call(press(KeyCode.LEFT))
-          .expectSpeech('c')
-          .call(press(KeyCode.LEFT))
-          .expectSpeech('b')
-          .call(press(KeyCode.LEFT))
-          .expectSpeech('a')
+        .call(press(KeyCode.LEFT))
+        .expectSpeech('3')
+        .call(press(KeyCode.LEFT))
+        .expectSpeech('2')
+        .call(press(KeyCode.LEFT))
+        .expectSpeech('1')
+        .call(press(KeyCode.LEFT))
+        .expectSpeech('\n')
+        .call(press(KeyCode.LEFT))
+        .expectSpeech('c')
+        .call(press(KeyCode.LEFT))
+        .expectSpeech('b')
+        .call(press(KeyCode.LEFT))
+        .expectSpeech('a')
 
-          .replay();
-    }.bind(this));
-    contentEditable.focus();
-  });
+        .replay();
+  }.bind(this));
+  contentEditable.focus();
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'ShowContextMenuOnViewsTab', function() {
-  const mockFeedback = this.createMockFeedback();
-  const site = `<p>test</p>`;
-  this.runWithLoadedTree(site, function(root) {
-    const tabs = root.findAll({Role: RoleType.TAB});
-    assertTrue(tabs.length > 0);
-    tabs[0].showContextMenu();
-    mockFeedback.expectSpeech(/menu opened/).replay();
-  });
-});
+TEST_F(
+    'ChromeVoxBackgroundTest', 'ShowContextMenuOnViewsTab', async function() {
+      const mockFeedback = this.createMockFeedback();
+      const site = `<p>test</p>`;
+      const root = await this.runWithLoadedTree(site);
+      const tabs = root.findAll({Role: RoleType.TAB});
+      assertTrue(tabs.length > 0);
+      tabs[0].showContextMenu();
+      mockFeedback.expectSpeech(/menu opened/).replay();
+    });
 
-TEST_F('ChromeVoxBackgroundTest', 'SelectWithOptGroup', function() {
+TEST_F('ChromeVoxBackgroundTest', 'SelectWithOptGroup', async function() {
   const mockFeedback = this.createMockFeedback();
   const site = `
     <select>
@@ -3871,40 +3762,38 @@
       </optgroup>
     </select>
   `;
-  this.runWithLoadedTree(site, function() {
-    mockFeedback.expectSpeech('Tyrannosaurus', 'has pop up', 'Collapsed')
-        .call(doCmd('forceClickOnCurrentItem'))
-        .expectSpeech('Tyrannosaurus')
-        .call(press(KeyCode.DOWN))
-        .expectSpeech('Velociraptor')
-        .call(press(KeyCode.DOWN))
-        .expectSpeech('Deinonychus')
-        .call(press(KeyCode.UP))
-        .expectSpeech('Velociraptor')
-        .replay();
-  });
+  await this.runWithLoadedTree(site);
+  mockFeedback.expectSpeech('Tyrannosaurus', 'has pop up', 'Collapsed')
+      .call(doCmd('forceClickOnCurrentItem'))
+      .expectSpeech('Tyrannosaurus')
+      .call(press(KeyCode.DOWN))
+      .expectSpeech('Velociraptor')
+      .call(press(KeyCode.DOWN))
+      .expectSpeech('Deinonychus')
+      .call(press(KeyCode.UP))
+      .expectSpeech('Velociraptor')
+      .replay();
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'GroupNavigation', function() {
+TEST_F('ChromeVoxBackgroundTest', 'GroupNavigation', async function() {
   const mockFeedback = this.createMockFeedback();
   const site = `
     <p><span>hello</span><a href="a.com">hi</a><a href="a.com">hey</a></p>
     <p><span>goodbye</span><a href="a.com">bye</a><a href="a.com">chow</a></p>
   `;
-  this.runWithLoadedTree(site, function() {
-    mockFeedback.call(doCmd('nextGroup'))
-        .expectSpeech('goodbye', 'bye', 'Link', 'chow', 'Link')
-        .call(doCmd('nextObject'))
-        .expectSpeech('goodbye')
-        .call(doCmd('nextObject'))
-        .expectSpeech('bye', 'Link')
-        .call(doCmd('previousGroup'))
-        .expectSpeech('hello', 'hi', 'Link', 'hey', 'Link')
-        .replay();
-  });
+  await this.runWithLoadedTree(site);
+  mockFeedback.call(doCmd('nextGroup'))
+      .expectSpeech('goodbye', 'bye', 'Link', 'chow', 'Link')
+      .call(doCmd('nextObject'))
+      .expectSpeech('goodbye')
+      .call(doCmd('nextObject'))
+      .expectSpeech('bye', 'Link')
+      .call(doCmd('previousGroup'))
+      .expectSpeech('hello', 'hi', 'Link', 'hey', 'Link')
+      .replay();
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'AllowIframeToBeFocused', function(root) {
+TEST_F('ChromeVoxBackgroundTest', 'AllowIframeToBeFocused', async function() {
   const mockFeedback = this.createMockFeedback();
   const site = `
     <p>hello</p>
@@ -3918,13 +3807,12 @@
       });
     </script>
   `;
-  this.runWithLoadedTree(site, function(root) {
-    const button = root.find({role: RoleType.BUTTON});
-    mockFeedback.expectSpeech('hello')
-        .call(button.doDefault.bind(button))
-        .expectSpeech('test title')
-        .replay();
-  });
+  const root = await this.runWithLoadedTree(site);
+  const button = root.find({role: RoleType.BUTTON});
+  mockFeedback.expectSpeech('hello')
+      .call(button.doDefault.bind(button))
+      .expectSpeech('test title')
+      .replay();
 });
 
 TEST_F('ChromeVoxBackgroundTest', 'NewWindowWebSpeech', function() {
@@ -3966,7 +3854,7 @@
   })();
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'MultipleListBoxes', function() {
+TEST_F('ChromeVoxBackgroundTest', 'MultipleListBoxes', async function() {
   const mockFeedback = this.createMockFeedback();
   const site = `
     <p>start</p>
@@ -4021,126 +3909,120 @@
       </div>
     </div>
   `;
-  this.runWithLoadedTree(site, function(root) {
-    mockFeedback.call(press(KeyCode.TAB))
-        .expectSpeech(
-            'Listbox item 1', ' 1 of 3 ', 'Configuration 1', 'List box')
-        .call(press(KeyCode.TAB))
-        .expectSpeech(
-            'Listbox item 2', ' 2 of 3 ', 'Configuration 2', 'List box')
-        .call(press(KeyCode.TAB))
-        .expectSpeech(
-            'Listbox item 3', ' 3 of 3 ', 'Configuration 3', 'List box')
-        .replay();
-  });
+  await this.runWithLoadedTree(site);
+  mockFeedback.call(press(KeyCode.TAB))
+      .expectSpeech('Listbox item 1', ' 1 of 3 ', 'Configuration 1', 'List box')
+      .call(press(KeyCode.TAB))
+      .expectSpeech('Listbox item 2', ' 2 of 3 ', 'Configuration 2', 'List box')
+      .call(press(KeyCode.TAB))
+      .expectSpeech('Listbox item 3', ' 3 of 3 ', 'Configuration 3', 'List box')
+      .replay();
 });
 
 // Make sure linear navigation does not go inside ListBox's options.
-TEST_F('ChromeVoxBackgroundTest', 'ListBoxLinearNavigation', function() {
+TEST_F('ChromeVoxBackgroundTest', 'ListBoxLinearNavigation', async function() {
   const mockFeedback = this.createMockFeedback();
-  const site =
-
-      this.runWithLoadedTree(this.listBoxDoc, function(root) {
-        mockFeedback.call(doCmd('nextObject'))
-            .expectSpeech('Select an item', 'List box')
-            .call(doCmd('nextObject'))
-            .expectSpeech('Click', 'Button')
-            .call(doCmd('previousObject'))
-            .expectSpeech('Select an item', 'List box')
-            .replay();
-      });
+  await this.runWithLoadedTree(this.listBoxDoc);
+  mockFeedback.call(doCmd('nextObject'))
+      .expectSpeech('Select an item', 'List box')
+      .call(doCmd('nextObject'))
+      .expectSpeech('Click', 'Button')
+      .call(doCmd('previousObject'))
+      .expectSpeech('Select an item', 'List box')
+      .replay();
 });
 
 // Make sure navigation with Tab to ListBox lands on options.
-TEST_F('ChromeVoxBackgroundTest', 'ListBoxItemsNavigation', function() {
+TEST_F('ChromeVoxBackgroundTest', 'ListBoxItemsNavigation', async function() {
   const mockFeedback = this.createMockFeedback();
 
-  this.runWithLoadedTree(this.listBoxDoc, function(root) {
-    mockFeedback.call(press(KeyCode.TAB))
-        .expectSpeech(
-            'Listbox item one', ' 1 of 3 ', 'Select an item', 'List box')
-        .call(doCmd('nextObject'))
-        .expectSpeech('Listbox item two', ' 2 of 3 ')
-        .call(doCmd('nextObject'))
-        .expectSpeech('Listbox item three', ' 3 of 3 ')
-        .replay();
-  });
+  await this.runWithLoadedTree(this.listBoxDoc);
+  mockFeedback.call(press(KeyCode.TAB))
+      .expectSpeech(
+          'Listbox item one', ' 1 of 3 ', 'Select an item', 'List box')
+      .call(doCmd('nextObject'))
+      .expectSpeech('Listbox item two', ' 2 of 3 ')
+      .call(doCmd('nextObject'))
+      .expectSpeech('Listbox item three', ' 3 of 3 ')
+      .replay();
 });
 
-TEST_F('ChromeVoxBackgroundTest', 'CrossWindowNextPreviousFocus', function() {
-  const mockFeedback = this.createMockFeedback();
-  const site = `
+TEST_F(
+    'ChromeVoxBackgroundTest', 'CrossWindowNextPreviousFocus',
+    async function() {
+      const mockFeedback = this.createMockFeedback();
+      const site = `
 <div aria-label="first"><button>second</button><button>third</button></div>
 <div aria-label="fourth"><button>fifth</button><button>sixth</button></div>
 `;
-  this.runWithLoadedTree(site, function(root) {
-    // Fake out the divs to be windows.
-    const window1 = root.children[0];
-    const window2 = root.children[1];
-    Object.defineProperty(window1, 'role', {get: () => RoleType.WINDOW});
-    Object.defineProperty(window2, 'role', {get: () => RoleType.WINDOW});
+      const root = await this.runWithLoadedTree(site);
+      // Fake out the divs to be windows.
+      const window1 = root.children[0];
+      const window2 = root.children[1];
+      Object.defineProperty(window1, 'role', {get: () => RoleType.WINDOW});
+      Object.defineProperty(window2, 'role', {get: () => RoleType.WINDOW});
 
-    // Linear nav should just wrap inside the first window.
-    mockFeedback.call(doCmd('nextObject'))
-        .expectSpeech('third', 'Button')
+      // Linear nav should just wrap inside the first window.
+      mockFeedback.call(doCmd('nextObject'))
+          .expectSpeech('third', 'Button')
 
-        // Wrap.
-        .call(doCmd('nextObject'))
-        .expectSpeech('second', 'Button', 'first, window')
+          // Wrap.
+          .call(doCmd('nextObject'))
+          .expectSpeech('second', 'Button', 'first, window')
 
-        // Wrap.
-        .call(doCmd('previousObject'))
-        .expectSpeech('third', 'Button')
+          // Wrap.
+          .call(doCmd('previousObject'))
+          .expectSpeech('third', 'Button')
 
-        .call(() => {
-          // Link the two "windows" with next/previous focus.
-          Object.defineProperty(window1, 'nextFocus', {get: () => window2});
-          Object.defineProperty(window2, 'previousFocus', {get: () => window1});
-        })
+          .call(() => {
+            // Link the two "windows" with next/previous focus.
+            Object.defineProperty(window1, 'nextFocus', {get: () => window2});
+            Object.defineProperty(
+                window2, 'previousFocus', {get: () => window1});
+          })
 
-        // window1 -> window2.
-        .call(doCmd('nextObject'))
-        .expectSpeech('fifth', 'Button', 'fourth, window')
+          // window1 -> window2.
+          .call(doCmd('nextObject'))
+          .expectSpeech('fifth', 'Button', 'fourth, window')
 
-        // window2 -> window1.
-        .call(doCmd('previousObject'))
-        .expectSpeech('third', 'Button', 'first, window')
+          // window2 -> window1.
+          .call(doCmd('previousObject'))
+          .expectSpeech('third', 'Button', 'first, window')
 
-        .call(() => {
-          // Link the two "windows" with next/previous focus in a slightly
-          // different way.
-          Object.defineProperty(window1, 'previousFocus', {get: () => window2});
-          Object.defineProperty(window2, 'nextFocus', {get: () => window1});
-        })
+          .call(() => {
+            // Link the two "windows" with next/previous focus in a slightly
+            // different way.
+            Object.defineProperty(
+                window1, 'previousFocus', {get: () => window2});
+            Object.defineProperty(window2, 'nextFocus', {get: () => window1});
+          })
 
-        .call(doCmd('previousObject'))
-        .expectSpeech('second', 'Button')
+          .call(doCmd('previousObject'))
+          .expectSpeech('second', 'Button')
 
-        // window1 -> window2.
-        .call(doCmd('previousObject'))
-        .expectSpeech('sixth', 'Button', 'fourth, window')
+          // window1 -> window2.
+          .call(doCmd('previousObject'))
+          .expectSpeech('sixth', 'Button', 'fourth, window')
 
-        // window2 -> window1.
-        .call(doCmd('nextObject'))
-        .expectSpeech('second', 'Button', 'first, window')
+          // window2 -> window1.
+          .call(doCmd('nextObject'))
+          .expectSpeech('second', 'Button', 'first, window')
 
-        .replay();
-  });
-});
+          .replay();
+    });
 
-TEST_F('ChromeVoxBackgroundTest', 'GestureOnPopUpButton', function() {
+TEST_F('ChromeVoxBackgroundTest', 'GestureOnPopUpButton', async function() {
   const mockFeedback = this.createMockFeedback();
   const site = `
     <select><option>apple</option><option>banana</option></select>
   `;
-  this.runWithLoadedTree(site, function(root) {
-    mockFeedback.expectSpeech('Button', 'has pop up')
-        .call(doGesture(Gesture.CLICK))
-        .expectSpeech('Button', 'has pop up', 'Expanded')
-        .call(doGesture(Gesture.SWIPE_DOWN1))
-        .expectSpeech('banana')
-        .call(doGesture(Gesture.SWIPE_UP1))
-        .expectSpeech('apple')
-        .replay();
-  });
+  await this.runWithLoadedTree(site);
+  mockFeedback.expectSpeech('Button', 'has pop up')
+      .call(doGesture(Gesture.CLICK))
+      .expectSpeech('Button', 'has pop up', 'Expanded')
+      .call(doGesture(Gesture.SWIPE_DOWN1))
+      .expectSpeech('banana')
+      .call(doGesture(Gesture.SWIPE_UP1))
+      .expectSpeech('apple')
+      .replay();
 });
diff --git a/chrome/browser/resources/chromeos/accessibility/chromevox/background/classic_background.js b/chrome/browser/resources/chromeos/accessibility/chromevox/background/classic_background.js
index c4addc6..c62c6cd 100644
--- a/chrome/browser/resources/chromeos/accessibility/chromevox/background/classic_background.js
+++ b/chrome/browser/resources/chromeos/accessibility/chromevox/background/classic_background.js
@@ -6,6 +6,7 @@
  * @fileoverview Script that runs on the background page.
  */
 
+import {AbstractTts} from '../common/abstract_tts.js';
 import {CompositeTts} from '../common/composite_tts.js';
 import {ChromeVoxEditableTextBase, TypingEcho} from '../common/editable_text_base.js';
 import {TtsBackground} from '../common/tts_background.js';
diff --git a/chrome/browser/resources/chromeos/accessibility/chromevox/background/command_handler.js b/chrome/browser/resources/chromeos/accessibility/chromevox/background/command_handler.js
index 122cbd37..1bf5c5b 100644
--- a/chrome/browser/resources/chromeos/accessibility/chromevox/background/command_handler.js
+++ b/chrome/browser/resources/chromeos/accessibility/chromevox/background/command_handler.js
@@ -5,6 +5,8 @@
 /**
  * @fileoverview ChromeVox commands.
  */
+import {AbstractTts} from '../common/abstract_tts.js';
+import {CommandStore} from '../common/command_store.js';
 import {TypingEcho} from '../common/editable_text_base.js';
 import {ChromeVoxKbHandler} from '../common/keyboard_handler.js';
 
diff --git a/chrome/browser/resources/chromeos/accessibility/chromevox/background/desktop_automation_handler_test.js b/chrome/browser/resources/chromeos/accessibility/chromevox/background/desktop_automation_handler_test.js
index cb817d8..9afcc050 100644
--- a/chrome/browser/resources/chromeos/accessibility/chromevox/background/desktop_automation_handler_test.js
+++ b/chrome/browser/resources/chromeos/accessibility/chromevox/background/desktop_automation_handler_test.js
@@ -42,49 +42,46 @@
 
 TEST_F(
     'ChromeVoxDesktopAutomationHandlerTest', 'OnValueChangedSlider',
-    function() {
+    async function() {
       const mockFeedback = this.createMockFeedback();
       const site = `<input type="range"></input>`;
-      this.runWithLoadedTree(site, function(root) {
-        const slider = root.find({role: RoleType.SLIDER});
-        assertTrue(!!slider);
+      const root = await this.runWithLoadedTree(site);
+      const slider = root.find({role: RoleType.SLIDER});
+      assertTrue(!!slider);
 
-        let sliderValue = '50%';
-        Object.defineProperty(slider, 'value', {get: () => sliderValue});
+      let sliderValue = '50%';
+      Object.defineProperty(slider, 'value', {get: () => sliderValue});
 
-        const event =
-            new CustomAutomationEvent(EventType.VALUE_CHANGED, slider);
-        mockFeedback.call(() => this.handler_.onValueChanged(event))
-            .expectSpeech('Slider', '50%')
+      const event = new CustomAutomationEvent(EventType.VALUE_CHANGED, slider);
+      mockFeedback.call(() => this.handler_.onValueChanged(event))
+          .expectSpeech('Slider', '50%')
 
-            // Override the min time to observe value changes so that even super
-            // fast updates triggers speech.
-            .call(() => DesktopAutomationHandler.MIN_VALUE_CHANGE_DELAY_MS = -1)
-            .call(() => sliderValue = '60%')
-            .call(() => this.handler_.onValueChanged(event))
+          // Override the min time to observe value changes so that even super
+          // fast updates triggers speech.
+          .call(() => DesktopAutomationHandler.MIN_VALUE_CHANGE_DELAY_MS = -1)
+          .call(() => sliderValue = '60%')
+          .call(() => this.handler_.onValueChanged(event))
 
-            // The range stays on the slider, so subsequent value changes only
-            // report the value.
-            .expectNextSpeechUtteranceIsNot('Slider')
-            .expectSpeech('60%')
+          // The range stays on the slider, so subsequent value changes only
+          // report the value.
+          .expectNextSpeechUtteranceIsNot('Slider')
+          .expectSpeech('60%')
 
-            // Set the min time and send a value change which should be ignored.
-            .call(
-                () => DesktopAutomationHandler.MIN_VALUE_CHANGE_DELAY_MS =
-                    10000)
-            .call(() => sliderValue = '70%')
-            .call(() => this.handler_.onValueChanged(event))
+          // Set the min time and send a value change which should be ignored.
+          .call(
+              () => DesktopAutomationHandler.MIN_VALUE_CHANGE_DELAY_MS = 10000)
+          .call(() => sliderValue = '70%')
+          .call(() => this.handler_.onValueChanged(event))
 
-            // Send one more that is processed.
-            .call(() => DesktopAutomationHandler.MIN_VALUE_CHANGE_DELAY_MS = -1)
-            .call(() => sliderValue = '80%')
-            .call(() => this.handler_.onValueChanged(event))
+          // Send one more that is processed.
+          .call(() => DesktopAutomationHandler.MIN_VALUE_CHANGE_DELAY_MS = -1)
+          .call(() => sliderValue = '80%')
+          .call(() => this.handler_.onValueChanged(event))
 
-            .expectNextSpeechUtteranceIsNot('70%')
-            .expectSpeech('80%')
+          .expectNextSpeechUtteranceIsNot('70%')
+          .expectSpeech('80%')
 
-            .replay();
-      });
+          .replay();
     });
 
 TEST_F(
@@ -113,63 +110,65 @@
     });
 
 // Ensures behavior when IME candidates are selected.
-TEST_F('ChromeVoxDesktopAutomationHandlerTest', 'ImeCandidate', function() {
-  const mockFeedback = this.createMockFeedback();
-  const site = `<button>First</button><button>Second</button>`;
-  this.runWithLoadedTree(site, function(root) {
-    const candidates = root.findAll({role: RoleType.BUTTON});
-    const first = candidates[0];
-    const second = candidates[1];
-    assertNotNullNorUndefined(first);
-    assertNotNullNorUndefined(second);
-    // Fake roles to imitate IME candidates.
-    Object.defineProperty(first, 'role', {get: () => RoleType.IME_CANDIDATE});
-    Object.defineProperty(second, 'role', {get: () => RoleType.IME_CANDIDATE});
-    const selectFirst = new CustomAutomationEvent(EventType.SELECTION, first);
-    const selectSecond = new CustomAutomationEvent(EventType.SELECTION, second);
-    mockFeedback.call(() => this.handler_.onSelection(selectFirst))
-        .expectSpeech('First')
-        .expectSpeech('F: foxtrot, i: india, r: romeo, s: sierra, t: tango')
-        .call(() => this.handler_.onSelection(selectSecond))
-        .expectSpeech('Second')
-        .expectSpeech(
-            'S: sierra, e: echo, c: charlie, o: oscar, n: november, d: delta')
-        .call(() => this.handler_.onSelection(selectFirst))
-        .expectSpeech('First')
-        .expectSpeech(/foxtrot/)
-        .replay();
-  });
-});
-
 TEST_F(
-    'ChromeVoxDesktopAutomationHandlerTest', 'IgnoreRepeatedAlerts',
-    function() {
+    'ChromeVoxDesktopAutomationHandlerTest', 'ImeCandidate', async function() {
       const mockFeedback = this.createMockFeedback();
-      const site = `<button>Hello world</button>`;
-      this.runWithLoadedTree(site, function(root) {
-        const button = root.find({role: RoleType.BUTTON});
-        assertTrue(!!button);
-        const event = new CustomAutomationEvent(EventType.ALERT, button);
-        mockFeedback
-            .call(() => {
-              DesktopAutomationHandler.MIN_ALERT_DELAY_MS = 20 * 1000;
-              this.handler_.onAlert(event);
-            })
-            .expectSpeech('Hello world')
-            .clearPendingOutput()
-            .call(() => {
-              // Repeated alerts should be ignored.
-              this.handler_.onAlert(event);
-              assertFalse(mockFeedback.utteranceInQueue('Hello world'));
-              this.handler_.onAlert(event);
-              assertFalse(mockFeedback.utteranceInQueue('Hello world'));
-            })
-            .replay();
-      });
+      const site = `<button>First</button><button>Second</button>`;
+      const root = await this.runWithLoadedTree(site);
+      const candidates = root.findAll({role: RoleType.BUTTON});
+      const first = candidates[0];
+      const second = candidates[1];
+      assertNotNullNorUndefined(first);
+      assertNotNullNorUndefined(second);
+      // Fake roles to imitate IME candidates.
+      Object.defineProperty(first, 'role', {get: () => RoleType.IME_CANDIDATE});
+      Object.defineProperty(
+          second, 'role', {get: () => RoleType.IME_CANDIDATE});
+      const selectFirst = new CustomAutomationEvent(EventType.SELECTION, first);
+      const selectSecond =
+          new CustomAutomationEvent(EventType.SELECTION, second);
+      mockFeedback.call(() => this.handler_.onSelection(selectFirst))
+          .expectSpeech('First')
+          .expectSpeech('F: foxtrot, i: india, r: romeo, s: sierra, t: tango')
+          .call(() => this.handler_.onSelection(selectSecond))
+          .expectSpeech('Second')
+          .expectSpeech(
+              'S: sierra, e: echo, c: charlie, o: oscar, n: november, d: delta')
+          .call(() => this.handler_.onSelection(selectFirst))
+          .expectSpeech('First')
+          .expectSpeech(/foxtrot/)
+          .replay();
     });
 
 TEST_F(
-    'ChromeVoxDesktopAutomationHandlerTest', 'DatalistSelection', function() {
+    'ChromeVoxDesktopAutomationHandlerTest', 'IgnoreRepeatedAlerts',
+    async function() {
+      const mockFeedback = this.createMockFeedback();
+      const site = `<button>Hello world</button>`;
+      const root = await this.runWithLoadedTree(site);
+      const button = root.find({role: RoleType.BUTTON});
+      assertTrue(!!button);
+      const event = new CustomAutomationEvent(EventType.ALERT, button);
+      mockFeedback
+          .call(() => {
+            DesktopAutomationHandler.MIN_ALERT_DELAY_MS = 20 * 1000;
+            this.handler_.onAlert(event);
+          })
+          .expectSpeech('Hello world')
+          .clearPendingOutput()
+          .call(() => {
+            // Repeated alerts should be ignored.
+            this.handler_.onAlert(event);
+            assertFalse(mockFeedback.utteranceInQueue('Hello world'));
+            this.handler_.onAlert(event);
+            assertFalse(mockFeedback.utteranceInQueue('Hello world'));
+          })
+          .replay();
+    });
+
+TEST_F(
+    'ChromeVoxDesktopAutomationHandlerTest', 'DatalistSelection',
+    async function() {
       const mockFeedback = this.createMockFeedback();
       const site = `
     <input aria-label="Choose one" list="list">
@@ -178,25 +177,24 @@
     <option>bar</option>
     </datalist>
   `;
-      this.runWithLoadedTree(site, async function(root) {
-        const combobox = root.find({
-          role: RoleType.TEXT_FIELD_WITH_COMBO_BOX,
-          attributes: {name: 'Choose one'}
-        });
-        assertTrue(!!combobox);
-        combobox.focus();
-        await new Promise(r => combobox.addEventListener(EventType.FOCUS, r));
-
-        // The combobox is now actually focused, safe to send arrows.
-        mockFeedback.call(press(KeyCode.DOWN))
-            .expectSpeech('foo', 'List item', ' 1 of 2 ')
-            .expectBraille('foo lstitm 1/2 (x)')
-            .call(press(KeyCode.DOWN))
-            .expectSpeech('bar', 'List item', ' 2 of 2 ')
-            .expectBraille('bar lstitm 2/2 (x)')
-            .call(press(KeyCode.UP))
-            .expectSpeech('foo', 'List item', ' 1 of 2 ')
-            .expectBraille('foo lstitm 1/2 (x)')
-            .replay();
+      const root = await this.runWithLoadedTree(site);
+      const combobox = root.find({
+        role: RoleType.TEXT_FIELD_WITH_COMBO_BOX,
+        attributes: {name: 'Choose one'}
       });
+      assertTrue(!!combobox);
+      combobox.focus();
+      await new Promise(r => combobox.addEventListener(EventType.FOCUS, r));
+
+      // The combobox is now actually focused, safe to send arrows.
+      mockFeedback.call(press(KeyCode.DOWN))
+          .expectSpeech('foo', 'List item', ' 1 of 2 ')
+          .expectBraille('foo lstitm 1/2 (x)')
+          .call(press(KeyCode.DOWN))
+          .expectSpeech('bar', 'List item', ' 2 of 2 ')
+          .expectBraille('bar lstitm 2/2 (x)')
+          .call(press(KeyCode.UP))
+          .expectSpeech('foo', 'List item', ' 1 of 2 ')
+          .expectBraille('foo lstitm 1/2 (x)')
+          .replay();
     });
diff --git a/chrome/browser/resources/chromeos/accessibility/chromevox/background/editing/editing.js b/chrome/browser/resources/chromeos/accessibility/chromevox/background/editing/editing.js
index c72373d..9489df6 100644
--- a/chrome/browser/resources/chromeos/accessibility/chromevox/background/editing/editing.js
+++ b/chrome/browser/resources/chromeos/accessibility/chromevox/background/editing/editing.js
@@ -6,6 +6,7 @@
  * @fileoverview Processes events related to editing text and emits the
  * appropriate spoken and braille feedback.
  */
+import {AbstractTts} from '../../common/abstract_tts.js';
 import {ChromeVoxEditableTextBase, TextChangeEvent} from '../../common/editable_text_base.js';
 import {BrailleBackground} from '../braille_background.js';
 import {Color} from '../color.js';
diff --git a/chrome/browser/resources/chromeos/accessibility/chromevox/background/editing/editing_test.js b/chrome/browser/resources/chromeos/accessibility/chromevox/background/editing/editing_test.js
index bf80b15c..4ebbc483 100644
--- a/chrome/browser/resources/chromeos/accessibility/chromevox/background/editing/editing_test.js
+++ b/chrome/browser/resources/chromeos/accessibility/chromevox/background/editing/editing_test.js
@@ -66,55 +66,50 @@
 </textarea>
 `;
 
-TEST_F('ChromeVoxEditingTest', 'Focus', function() {
+TEST_F('ChromeVoxEditingTest', 'Focus', async function() {
   const mockFeedback = this.createMockFeedback();
-  this.runWithLoadedTree(doc, function(root) {
-    const singleLine = root.find(
-        {role: RoleType.TEXT_FIELD, attributes: {name: 'singleLine'}});
-    const textarea =
-        root.find({role: RoleType.TEXT_FIELD, attributes: {name: 'textArea'}});
-    singleLine.focus();
-    mockFeedback.expectSpeech('singleLine', 'Single line field', 'Edit text')
-        .expectBraille(
-            'singleLine Single line field ed', {startIndex: 11, endIndex: 11})
-        .call(textarea.focus.bind(textarea))
-        .expectSpeech('textArea', 'Line 1\nline 2\nline 3', 'Text area')
-        .expectBraille(
-            'textArea Line 1\nline 2\nline 3 mled',
-            {startIndex: 9, endIndex: 9});
+  const root = await this.runWithLoadedTree(doc);
+  const singleLine =
+      root.find({role: RoleType.TEXT_FIELD, attributes: {name: 'singleLine'}});
+  const textarea =
+      root.find({role: RoleType.TEXT_FIELD, attributes: {name: 'textArea'}});
+  singleLine.focus();
+  mockFeedback.expectSpeech('singleLine', 'Single line field', 'Edit text')
+      .expectBraille(
+          'singleLine Single line field ed', {startIndex: 11, endIndex: 11})
+      .call(textarea.focus.bind(textarea))
+      .expectSpeech('textArea', 'Line 1\nline 2\nline 3', 'Text area')
+      .expectBraille(
+          'textArea Line 1\nline 2\nline 3 mled', {startIndex: 9, endIndex: 9});
 
-    mockFeedback.replay();
-  });
+  mockFeedback.replay();
 });
 
-TEST_F('ChromeVoxEditingTest', 'Multiline', function() {
+TEST_F('ChromeVoxEditingTest', 'Multiline', async function() {
   const mockFeedback = this.createMockFeedback();
-  this.runWithLoadedTree(doc, function(root) {
-    const textarea =
-        root.find({role: RoleType.TEXT_FIELD, attributes: {name: 'textArea'}});
-    textarea.focus();
-    mockFeedback.expectSpeech('textArea', 'Line 1\nline 2\nline 3', 'Text area')
-        .expectBraille(
-            'textArea Line 1\nline 2\nline 3 mled',
-            {startIndex: 9, endIndex: 9})
-        .call(textarea.setSelection.bind(textarea, 1, 1))
-        .expectSpeech('i')
-        .expectBraille('Line 1\nmled', {startIndex: 1, endIndex: 1})
-        .call(textarea.setSelection.bind(textarea, 7, 7))
-        .expectSpeech('line 2')
-        .expectBraille('line 2\n', {startIndex: 0, endIndex: 0})
-        .call(textarea.setSelection.bind(textarea, 7, 13))
-        .expectSpeech('line 2', 'selected')
-        .expectBraille('line 2\n', {startIndex: 0, endIndex: 6});
+  const root = await this.runWithLoadedTree(doc);
+  const textarea =
+      root.find({role: RoleType.TEXT_FIELD, attributes: {name: 'textArea'}});
+  textarea.focus();
+  mockFeedback.expectSpeech('textArea', 'Line 1\nline 2\nline 3', 'Text area')
+      .expectBraille(
+          'textArea Line 1\nline 2\nline 3 mled', {startIndex: 9, endIndex: 9})
+      .call(textarea.setSelection.bind(textarea, 1, 1))
+      .expectSpeech('i')
+      .expectBraille('Line 1\nmled', {startIndex: 1, endIndex: 1})
+      .call(textarea.setSelection.bind(textarea, 7, 7))
+      .expectSpeech('line 2')
+      .expectBraille('line 2\n', {startIndex: 0, endIndex: 0})
+      .call(textarea.setSelection.bind(textarea, 7, 13))
+      .expectSpeech('line 2', 'selected')
+      .expectBraille('line 2\n', {startIndex: 0, endIndex: 6});
 
-    mockFeedback.replay();
-  });
+  mockFeedback.replay();
 });
 
-TEST_F('ChromeVoxEditingTest', 'TextButNoSelectionChange', function() {
+TEST_F('ChromeVoxEditingTest', 'TextButNoSelectionChange', async function() {
   const mockFeedback = this.createMockFeedback();
-  this.runWithLoadedTree(
-      `
+  const root = await this.runWithLoadedTree(`
       <h1>Test doc</h1>
       <input type='text' id='input' value='text1'>
       <!-- We don't seem to get an event in js when the automation
@@ -133,26 +128,23 @@
         }
         timer = window.setInterval(poll, 200);
       </script>
-    `,
-      function(root) {
-        const input = root.find({role: RoleType.TEXT_FIELD});
-        input.focus();
-        mockFeedback.expectSpeech('text1', 'Edit text')
-            .expectBraille('text1 ed', {startIndex: 0, endIndex: 0})
-            .call(input.setSelection.bind(input, 5, 5))
-            .expectBraille('text2 ed', {startIndex: 5, endIndex: 5});
+    `);
+  const input = root.find({role: RoleType.TEXT_FIELD});
+  input.focus();
+  mockFeedback.expectSpeech('text1', 'Edit text')
+      .expectBraille('text1 ed', {startIndex: 0, endIndex: 0})
+      .call(input.setSelection.bind(input, 5, 5))
+      .expectBraille('text2 ed', {startIndex: 5, endIndex: 5});
 
-        mockFeedback.replay();
-      });
+  mockFeedback.replay();
 });
 
-TEST_F('ChromeVoxEditingTest', 'RichTextMoveByLine', function() {
+TEST_F('ChromeVoxEditingTest', 'RichTextMoveByLine', async function() {
   // Turn on rich text output settings.
   localStorage['announceRichTextAttributes'] = 'true';
 
   const mockFeedback = this.createMockFeedback();
-  this.runWithLoadedTree(
-      `
+  const root = await this.runWithLoadedTree(`
     <div role="textbox" contenteditable>
       <h2>hello</h2>
       <div><br></div>
@@ -179,35 +171,32 @@
         }
       }, true);
     </script>
-  `,
-      async function(root) {
-        await this.focusFirstTextField(root);
+  `);
+  await this.focusFirstTextField(root);
 
-        const go = root.find({role: RoleType.BUTTON});
-        const moveByLine = go.doDefault.bind(go);
-        mockFeedback.call(moveByLine)
-            .expectSpeech('\n')
-            .expectBraille('\n')
-            .call(moveByLine)
-            .expectSpeech('This is a ', 'test', 'Link', ' of rich text')
-            .expectBraille('This is a test of rich text')
-            .call(moveByLine)
-            .expectSpeech('\n')
-            .expectBraille('\n')
-            .call(moveByLine)
-            .expectSpeech('hello', 'Heading 2')
-            .expectBraille('hello h2 mled')
-            .replay();
-      });
+  const go = root.find({role: RoleType.BUTTON});
+  const moveByLine = go.doDefault.bind(go);
+  mockFeedback.call(moveByLine)
+      .expectSpeech('\n')
+      .expectBraille('\n')
+      .call(moveByLine)
+      .expectSpeech('This is a ', 'test', 'Link', ' of rich text')
+      .expectBraille('This is a test of rich text')
+      .call(moveByLine)
+      .expectSpeech('\n')
+      .expectBraille('\n')
+      .call(moveByLine)
+      .expectSpeech('hello', 'Heading 2')
+      .expectBraille('hello h2 mled')
+      .replay();
 });
 
-TEST_F('ChromeVoxEditingTest', 'RichTextMoveByCharacter', function() {
+TEST_F('ChromeVoxEditingTest', 'RichTextMoveByCharacter', async function() {
   // Turn on rich text output settings.
   localStorage['announceRichTextAttributes'] = 'true';
 
   const mockFeedback = this.createMockFeedback();
-  this.runWithLoadedTree(
-      `
+  const root = await this.runWithLoadedTree(`
     <div role="textbox" contenteditable>This <b>is</b> a test.</div>
     <button id="go">Go</button>
 
@@ -231,61 +220,59 @@
         }
       }, true);
     </script>
-  `,
-      async function(root) {
-        await this.focusFirstTextField(root);
+  `);
+  await this.focusFirstTextField(root);
 
-        const go = root.find({role: RoleType.BUTTON});
-        const moveByChar = go.doDefault.bind(go);
-        const lineText = 'This is a test. mled';
+  const go = root.find({role: RoleType.BUTTON});
+  const moveByChar = go.doDefault.bind(go);
+  const lineText = 'This is a test. mled';
 
-        mockFeedback.call(moveByChar)
-            .expectSpeech('h')
-            .expectBraille(lineText, {startIndex: 1, endIndex: 1})
-            .call(moveByChar)
-            .expectSpeech('i')
-            .expectBraille(lineText, {startIndex: 2, endIndex: 2})
-            .call(moveByChar)
-            .expectSpeech('s')
-            .expectBraille(lineText, {startIndex: 3, endIndex: 3})
-            .call(moveByChar)
-            .expectSpeech(' ')
-            .expectBraille(lineText, {startIndex: 4, endIndex: 4})
+  mockFeedback.call(moveByChar)
+      .expectSpeech('h')
+      .expectBraille(lineText, {startIndex: 1, endIndex: 1})
+      .call(moveByChar)
+      .expectSpeech('i')
+      .expectBraille(lineText, {startIndex: 2, endIndex: 2})
+      .call(moveByChar)
+      .expectSpeech('s')
+      .expectBraille(lineText, {startIndex: 3, endIndex: 3})
+      .call(moveByChar)
+      .expectSpeech(' ')
+      .expectBraille(lineText, {startIndex: 4, endIndex: 4})
 
-            .call(moveByChar)
-            .expectSpeech('i')
-            .expectSpeech('Bold')
-            .expectBraille(lineText, {startIndex: 5, endIndex: 5})
+      .call(moveByChar)
+      .expectSpeech('i')
+      .expectSpeech('Bold')
+      .expectBraille(lineText, {startIndex: 5, endIndex: 5})
 
-            .call(moveByChar)
-            .expectSpeech('s')
-            .expectBraille(lineText, {startIndex: 6, endIndex: 6})
+      .call(moveByChar)
+      .expectSpeech('s')
+      .expectBraille(lineText, {startIndex: 6, endIndex: 6})
 
-            .call(moveByChar)
-            .expectSpeech(' ')
-            .expectSpeech('Not bold')
-            .expectBraille(lineText, {startIndex: 7, endIndex: 7})
+      .call(moveByChar)
+      .expectSpeech(' ')
+      .expectSpeech('Not bold')
+      .expectBraille(lineText, {startIndex: 7, endIndex: 7})
 
-            .call(moveByChar)
-            .expectSpeech('a')
-            .expectBraille(lineText, {startIndex: 8, endIndex: 8})
+      .call(moveByChar)
+      .expectSpeech('a')
+      .expectBraille(lineText, {startIndex: 8, endIndex: 8})
 
-            .call(moveByChar)
-            .expectSpeech(' ')
-            .expectBraille(lineText, {startIndex: 9, endIndex: 9})
+      .call(moveByChar)
+      .expectSpeech(' ')
+      .expectBraille(lineText, {startIndex: 9, endIndex: 9})
 
-            .replay();
-      });
+      .replay();
 });
 
 TEST_F(
-    'ChromeVoxEditingTest', 'RichTextMoveByCharacterAllAttributes', function() {
+    'ChromeVoxEditingTest', 'RichTextMoveByCharacterAllAttributes',
+    async function() {
       // Turn on rich text output settings.
       localStorage['announceRichTextAttributes'] = 'true';
 
       const mockFeedback = this.createMockFeedback();
-      this.runWithLoadedTree(
-          `
+      const root = await this.runWithLoadedTree(`
     <div role="textbox" contenteditable>
       <p style="font-size:20px; font-family:times">
         <b style="color:#ff0000">Move</b>
@@ -303,157 +290,153 @@
         sel.modify('move', 'forward', 'character');
       }, true);
     </script>
-  `,
-          async function(root) {
-            await this.focusFirstTextField(root);
+  `);
+      await this.focusFirstTextField(root);
 
-            const go = root.find({role: RoleType.BUTTON});
-            const moveByChar = go.doDefault.bind(go);
-            const lineText = 'Move through text by character test! mled';
-            const lineOnLinkText =
-                'Move through text by character test lnk ! mled';
+      const go = root.find({role: RoleType.BUTTON});
+      const moveByChar = go.doDefault.bind(go);
+      const lineText = 'Move through text by character test! mled';
+      const lineOnLinkText = 'Move through text by character test lnk ! mled';
 
-            mockFeedback.call(moveByChar)
-                .expectSpeech('o')
-                .expectSpeech('Size 20')
-                .expectSpeech('Red, 100% opacity.')
-                .expectSpeech('Bold')
-                .expectSpeech('Font Tinos')
-                .expectBraille(lineText, {startIndex: 1, endIndex: 1})
-                .call(moveByChar)
-                .expectSpeech('v')
-                .expectBraille(lineText, {startIndex: 2, endIndex: 2})
-                .call(moveByChar)
-                .expectSpeech('e')
-                .expectBraille(lineText, {startIndex: 3, endIndex: 3})
-                .call(moveByChar)
-                .expectSpeech(' ')
-                .expectSpeech('Black, 100% opacity.')
-                .expectSpeech('Not bold')
-                .expectBraille(lineText, {startIndex: 4, endIndex: 4})
-                .call(moveByChar)
-                .expectSpeech('t')
-                .expectSpeech('Italic')
-                .expectBraille(lineText, {startIndex: 5, endIndex: 5})
-                .call(moveByChar)
-                .expectSpeech('h')
-                .expectBraille(lineText, {startIndex: 6, endIndex: 6})
-                .call(moveByChar)
-                .expectSpeech('r')
-                .expectBraille(lineText, {startIndex: 7, endIndex: 7})
-                .call(moveByChar)
-                .expectSpeech('o')
-                .expectBraille(lineText, {startIndex: 8, endIndex: 8})
-                .call(moveByChar)
-                .expectSpeech('u')
-                .expectBraille(lineText, {startIndex: 9, endIndex: 9})
-                .call(moveByChar)
-                .expectSpeech('g')
-                .expectBraille(lineText, {startIndex: 10, endIndex: 10})
-                .call(moveByChar)
-                .expectSpeech('h')
-                .expectBraille(lineText, {startIndex: 11, endIndex: 11})
-                .call(moveByChar)
-                .expectSpeech(' ')
-                .expectSpeech('Not italic')
-                .expectBraille(lineText, {startIndex: 12, endIndex: 12})
-                .call(moveByChar)
-                .expectSpeech('t')
-                .expectSpeech('Underline')
-                .expectSpeech('Font Gelasio')
-                .expectBraille(lineText, {startIndex: 13, endIndex: 13})
-                .call(moveByChar)
-                .expectSpeech('e')
-                .expectBraille(lineText, {startIndex: 14, endIndex: 14})
-                .call(moveByChar)
-                .expectSpeech('x')
-                .expectBraille(lineText, {startIndex: 15, endIndex: 15})
-                .call(moveByChar)
-                .expectSpeech('t')
-                .expectBraille(lineText, {startIndex: 16, endIndex: 16})
-                .call(moveByChar)
-                .expectSpeech(' ')
-                .expectSpeech('Not underline')
-                .expectSpeech('Font Tinos')
-                .expectBraille(lineText, {startIndex: 17, endIndex: 17})
-                .call(moveByChar)
-                .expectSpeech('b')
-                .expectBraille(lineText, {startIndex: 18, endIndex: 18})
-                .call(moveByChar)
-                .expectSpeech('y')
-                .expectBraille(lineText, {startIndex: 19, endIndex: 19})
-                .call(moveByChar)
-                .expectSpeech(' ')
-                .expectBraille(lineText, {startIndex: 20, endIndex: 20})
-                .call(moveByChar)
-                .expectSpeech('c')
-                .expectSpeech('Size 12')
-                .expectSpeech('Blue, 100% opacity.')
-                .expectSpeech('Line through')
-                .expectBraille(lineText, {startIndex: 21, endIndex: 21})
-                .call(moveByChar)
-                .expectSpeech('h')
-                .expectBraille(lineText, {startIndex: 22, endIndex: 22})
-                .call(moveByChar)
-                .expectSpeech('a')
-                .expectBraille(lineText, {startIndex: 23, endIndex: 23})
-                .call(moveByChar)
-                .expectSpeech('r')
-                .expectBraille(lineText, {startIndex: 24, endIndex: 24})
-                .call(moveByChar)
-                .expectSpeech('a')
-                .expectBraille(lineText, {startIndex: 25, endIndex: 25})
-                .call(moveByChar)
-                .expectSpeech('c')
-                .expectBraille(lineText, {startIndex: 26, endIndex: 26})
-                .call(moveByChar)
-                .expectSpeech('t')
-                .expectBraille(lineText, {startIndex: 27, endIndex: 27})
-                .call(moveByChar)
-                .expectSpeech('e')
-                .expectBraille(lineText, {startIndex: 28, endIndex: 28})
-                .call(moveByChar)
-                .expectSpeech('r')
-                .expectBraille(lineText, {startIndex: 29, endIndex: 29})
-                .call(moveByChar)
-                .expectSpeech(' ')
-                .expectSpeech('Size 20')
-                .expectSpeech('Black, 100% opacity.')
-                .expectSpeech('Not line through')
-                .expectBraille(lineText, {startIndex: 30, endIndex: 30})
-                .call(moveByChar)
-                .expectSpeech('t')
-                .expectSpeech('Blue, 100% opacity.')
-                .expectSpeech('Link')
-                .expectSpeech('Underline')
-                .expectBraille(lineOnLinkText, {startIndex: 31, endIndex: 31})
-                .call(moveByChar)
-                .expectSpeech('e')
-                .expectBraille(lineOnLinkText, {startIndex: 32, endIndex: 32})
-                .call(moveByChar)
-                .expectSpeech('s')
-                .expectBraille(lineOnLinkText, {startIndex: 33, endIndex: 33})
-                .call(moveByChar)
-                .expectSpeech('t')
-                .expectBraille(lineOnLinkText, {startIndex: 34, endIndex: 34})
-                .call(moveByChar)
-                .expectSpeech('!')
-                .expectSpeech('Black, 100% opacity.')
-                .expectSpeech('Not link')
-                .expectSpeech('Not underline')
-                .expectBraille(lineText, {startIndex: 35, endIndex: 35})
+      mockFeedback.call(moveByChar)
+          .expectSpeech('o')
+          .expectSpeech('Size 20')
+          .expectSpeech('Red, 100% opacity.')
+          .expectSpeech('Bold')
+          .expectSpeech('Font Tinos')
+          .expectBraille(lineText, {startIndex: 1, endIndex: 1})
+          .call(moveByChar)
+          .expectSpeech('v')
+          .expectBraille(lineText, {startIndex: 2, endIndex: 2})
+          .call(moveByChar)
+          .expectSpeech('e')
+          .expectBraille(lineText, {startIndex: 3, endIndex: 3})
+          .call(moveByChar)
+          .expectSpeech(' ')
+          .expectSpeech('Black, 100% opacity.')
+          .expectSpeech('Not bold')
+          .expectBraille(lineText, {startIndex: 4, endIndex: 4})
+          .call(moveByChar)
+          .expectSpeech('t')
+          .expectSpeech('Italic')
+          .expectBraille(lineText, {startIndex: 5, endIndex: 5})
+          .call(moveByChar)
+          .expectSpeech('h')
+          .expectBraille(lineText, {startIndex: 6, endIndex: 6})
+          .call(moveByChar)
+          .expectSpeech('r')
+          .expectBraille(lineText, {startIndex: 7, endIndex: 7})
+          .call(moveByChar)
+          .expectSpeech('o')
+          .expectBraille(lineText, {startIndex: 8, endIndex: 8})
+          .call(moveByChar)
+          .expectSpeech('u')
+          .expectBraille(lineText, {startIndex: 9, endIndex: 9})
+          .call(moveByChar)
+          .expectSpeech('g')
+          .expectBraille(lineText, {startIndex: 10, endIndex: 10})
+          .call(moveByChar)
+          .expectSpeech('h')
+          .expectBraille(lineText, {startIndex: 11, endIndex: 11})
+          .call(moveByChar)
+          .expectSpeech(' ')
+          .expectSpeech('Not italic')
+          .expectBraille(lineText, {startIndex: 12, endIndex: 12})
+          .call(moveByChar)
+          .expectSpeech('t')
+          .expectSpeech('Underline')
+          .expectSpeech('Font Gelasio')
+          .expectBraille(lineText, {startIndex: 13, endIndex: 13})
+          .call(moveByChar)
+          .expectSpeech('e')
+          .expectBraille(lineText, {startIndex: 14, endIndex: 14})
+          .call(moveByChar)
+          .expectSpeech('x')
+          .expectBraille(lineText, {startIndex: 15, endIndex: 15})
+          .call(moveByChar)
+          .expectSpeech('t')
+          .expectBraille(lineText, {startIndex: 16, endIndex: 16})
+          .call(moveByChar)
+          .expectSpeech(' ')
+          .expectSpeech('Not underline')
+          .expectSpeech('Font Tinos')
+          .expectBraille(lineText, {startIndex: 17, endIndex: 17})
+          .call(moveByChar)
+          .expectSpeech('b')
+          .expectBraille(lineText, {startIndex: 18, endIndex: 18})
+          .call(moveByChar)
+          .expectSpeech('y')
+          .expectBraille(lineText, {startIndex: 19, endIndex: 19})
+          .call(moveByChar)
+          .expectSpeech(' ')
+          .expectBraille(lineText, {startIndex: 20, endIndex: 20})
+          .call(moveByChar)
+          .expectSpeech('c')
+          .expectSpeech('Size 12')
+          .expectSpeech('Blue, 100% opacity.')
+          .expectSpeech('Line through')
+          .expectBraille(lineText, {startIndex: 21, endIndex: 21})
+          .call(moveByChar)
+          .expectSpeech('h')
+          .expectBraille(lineText, {startIndex: 22, endIndex: 22})
+          .call(moveByChar)
+          .expectSpeech('a')
+          .expectBraille(lineText, {startIndex: 23, endIndex: 23})
+          .call(moveByChar)
+          .expectSpeech('r')
+          .expectBraille(lineText, {startIndex: 24, endIndex: 24})
+          .call(moveByChar)
+          .expectSpeech('a')
+          .expectBraille(lineText, {startIndex: 25, endIndex: 25})
+          .call(moveByChar)
+          .expectSpeech('c')
+          .expectBraille(lineText, {startIndex: 26, endIndex: 26})
+          .call(moveByChar)
+          .expectSpeech('t')
+          .expectBraille(lineText, {startIndex: 27, endIndex: 27})
+          .call(moveByChar)
+          .expectSpeech('e')
+          .expectBraille(lineText, {startIndex: 28, endIndex: 28})
+          .call(moveByChar)
+          .expectSpeech('r')
+          .expectBraille(lineText, {startIndex: 29, endIndex: 29})
+          .call(moveByChar)
+          .expectSpeech(' ')
+          .expectSpeech('Size 20')
+          .expectSpeech('Black, 100% opacity.')
+          .expectSpeech('Not line through')
+          .expectBraille(lineText, {startIndex: 30, endIndex: 30})
+          .call(moveByChar)
+          .expectSpeech('t')
+          .expectSpeech('Blue, 100% opacity.')
+          .expectSpeech('Link')
+          .expectSpeech('Underline')
+          .expectBraille(lineOnLinkText, {startIndex: 31, endIndex: 31})
+          .call(moveByChar)
+          .expectSpeech('e')
+          .expectBraille(lineOnLinkText, {startIndex: 32, endIndex: 32})
+          .call(moveByChar)
+          .expectSpeech('s')
+          .expectBraille(lineOnLinkText, {startIndex: 33, endIndex: 33})
+          .call(moveByChar)
+          .expectSpeech('t')
+          .expectBraille(lineOnLinkText, {startIndex: 34, endIndex: 34})
+          .call(moveByChar)
+          .expectSpeech('!')
+          .expectSpeech('Black, 100% opacity.')
+          .expectSpeech('Not link')
+          .expectSpeech('Not underline')
+          .expectBraille(lineText, {startIndex: 35, endIndex: 35})
 
-                .replay();
-          });
+          .replay();
     });
 
 // Tests specifically for cursor workarounds.
 TEST_F(
     'ChromeVoxEditingTest', 'RichTextMoveByCharacterNodeWorkaround',
-    function() {
+    async function() {
       const mockFeedback = this.createMockFeedback();
-      this.runWithLoadedTree(
-          `
+      const root = await this.runWithLoadedTree(`
     <div role="textbox" contenteditable>hello <b>world</b></div>
     <button id="go">Go</button>
 
@@ -463,40 +446,39 @@
         sel.modify('move', 'forward', 'character');
       }, true);
     </script>
-  `,
-          async function(root) {
-            await this.focusFirstTextField(root);
+  `);
+      await this.focusFirstTextField(root);
 
-            const go = root.find({role: RoleType.BUTTON});
-            const moveByChar = go.doDefault.bind(go);
-            const lineText = 'hello world mled';
+      const go = root.find({role: RoleType.BUTTON});
+      const moveByChar = go.doDefault.bind(go);
+      const lineText = 'hello world mled';
 
-            mockFeedback.call(moveByChar)
-                .expectSpeech('e')
-                .expectBraille(lineText, {startIndex: 1, endIndex: 1})
-                .call(moveByChar)
-                .expectSpeech('l')
-                .expectBraille(lineText, {startIndex: 2, endIndex: 2})
-                .call(moveByChar)
-                .expectSpeech('l')
-                .expectBraille(lineText, {startIndex: 3, endIndex: 3})
-                .call(moveByChar)
-                .expectSpeech('o')
-                .expectBraille(lineText, {startIndex: 4, endIndex: 4})
-                .call(moveByChar)
-                .expectSpeech(' ')
-                .expectBraille(lineText, {startIndex: 5, endIndex: 5})
-                .call(moveByChar)
-                .expectSpeech('w')
-                .expectBraille(lineText, {startIndex: 6, endIndex: 6})
-                .replay();
-          });
+      mockFeedback.call(moveByChar)
+          .expectSpeech('e')
+          .expectBraille(lineText, {startIndex: 1, endIndex: 1})
+          .call(moveByChar)
+          .expectSpeech('l')
+          .expectBraille(lineText, {startIndex: 2, endIndex: 2})
+          .call(moveByChar)
+          .expectSpeech('l')
+          .expectBraille(lineText, {startIndex: 3, endIndex: 3})
+          .call(moveByChar)
+          .expectSpeech('o')
+          .expectBraille(lineText, {startIndex: 4, endIndex: 4})
+          .call(moveByChar)
+          .expectSpeech(' ')
+          .expectBraille(lineText, {startIndex: 5, endIndex: 5})
+          .call(moveByChar)
+          .expectSpeech('w')
+          .expectBraille(lineText, {startIndex: 6, endIndex: 6})
+          .replay();
     });
 
-TEST_F('ChromeVoxEditingTest', 'RichTextMoveByCharacterEndOfLine', function() {
-  const mockFeedback = this.createMockFeedback();
-  this.runWithLoadedTree(
-      `
+TEST_F(
+    'ChromeVoxEditingTest', 'RichTextMoveByCharacterEndOfLine',
+    async function() {
+      const mockFeedback = this.createMockFeedback();
+      const root = await this.runWithLoadedTree(`
     <div role="textbox" contenteditable>Test</div>
     <button id="go">Go</button>
 
@@ -506,38 +488,35 @@
         sel.modify('move', 'forward', 'character');
       }, true);
     </script>
-  `,
-      async function(root) {
-        await this.focusFirstTextField(root);
+  `);
+      await this.focusFirstTextField(root);
 
-        const go = root.find({role: RoleType.BUTTON});
-        const moveByChar = go.doDefault.bind(go);
-        const lineText = 'Test mled';
+      const go = root.find({role: RoleType.BUTTON});
+      const moveByChar = go.doDefault.bind(go);
+      const lineText = 'Test mled';
 
-        mockFeedback.call(moveByChar)
-            .expectSpeech('e')
-            .expectBraille(lineText, {startIndex: 1, endIndex: 1})
-            .call(moveByChar)
-            .expectSpeech('s')
-            .expectBraille(lineText, {startIndex: 2, endIndex: 2})
-            .call(moveByChar)
-            .expectSpeech('t')
-            .expectBraille(lineText, {startIndex: 3, endIndex: 3})
-            .call(moveByChar)
-            .expectSpeech('End of text')
-            .expectBraille(lineText, {startIndex: 4, endIndex: 4})
+      mockFeedback.call(moveByChar)
+          .expectSpeech('e')
+          .expectBraille(lineText, {startIndex: 1, endIndex: 1})
+          .call(moveByChar)
+          .expectSpeech('s')
+          .expectBraille(lineText, {startIndex: 2, endIndex: 2})
+          .call(moveByChar)
+          .expectSpeech('t')
+          .expectBraille(lineText, {startIndex: 3, endIndex: 3})
+          .call(moveByChar)
+          .expectSpeech('End of text')
+          .expectBraille(lineText, {startIndex: 4, endIndex: 4})
 
-            .replay();
-      });
-});
+          .replay();
+    });
 
-TEST_F('ChromeVoxEditingTest', 'RichTextLinkOutput', function() {
+TEST_F('ChromeVoxEditingTest', 'RichTextLinkOutput', async function() {
   // Turn on rich text output settings.
   localStorage['announceRichTextAttributes'] = 'true';
 
   const mockFeedback = this.createMockFeedback();
-  this.runWithLoadedTree(
-      `
+  const root = await this.runWithLoadedTree(`
     <div role="textbox" contenteditable>a <a href="#">test</a></div>
     <button id="go">Go</button>
     <script>
@@ -546,42 +525,39 @@
         sel.modify('move', 'forward', 'character');
       }, true);
     </script>
-  `,
-      async function(root) {
-        await this.focusFirstTextField(root);
+  `);
+  await this.focusFirstTextField(root);
 
-        const go = root.find({role: RoleType.BUTTON});
-        const moveByChar = go.doDefault.bind(go);
-        const lineText = 'a test mled';
-        const lineOnLinkText = 'a test lnk mled';
+  const go = root.find({role: RoleType.BUTTON});
+  const moveByChar = go.doDefault.bind(go);
+  const lineText = 'a test mled';
+  const lineOnLinkText = 'a test lnk mled';
 
-        mockFeedback.call(moveByChar)
-            .expectSpeech(' ')
-            .expectBraille(lineText, {startIndex: 1, endIndex: 1})
-            .call(moveByChar)
-            .expectSpeech('t')
-            .expectSpeech('Blue, 100% opacity.')
-            .expectSpeech('Link')
-            .expectSpeech('Underline')
-            .expectBraille(lineOnLinkText, {startIndex: 2, endIndex: 2})
-            .call(moveByChar)
-            .expectSpeech('e')
-            .expectBraille(lineOnLinkText, {startIndex: 3, endIndex: 3})
-            .call(moveByChar)
-            .expectSpeech('s')
-            .expectBraille(lineOnLinkText, {startIndex: 4, endIndex: 4})
-            .call(moveByChar)
-            .expectSpeech('t')
-            .expectBraille(lineOnLinkText, {startIndex: 5, endIndex: 5})
+  mockFeedback.call(moveByChar)
+      .expectSpeech(' ')
+      .expectBraille(lineText, {startIndex: 1, endIndex: 1})
+      .call(moveByChar)
+      .expectSpeech('t')
+      .expectSpeech('Blue, 100% opacity.')
+      .expectSpeech('Link')
+      .expectSpeech('Underline')
+      .expectBraille(lineOnLinkText, {startIndex: 2, endIndex: 2})
+      .call(moveByChar)
+      .expectSpeech('e')
+      .expectBraille(lineOnLinkText, {startIndex: 3, endIndex: 3})
+      .call(moveByChar)
+      .expectSpeech('s')
+      .expectBraille(lineOnLinkText, {startIndex: 4, endIndex: 4})
+      .call(moveByChar)
+      .expectSpeech('t')
+      .expectBraille(lineOnLinkText, {startIndex: 5, endIndex: 5})
 
-            .replay();
-      });
+      .replay();
 });
 
-TEST_F('ChromeVoxEditingTest', 'RichTextExtendByCharacter', function() {
+TEST_F('ChromeVoxEditingTest', 'RichTextExtendByCharacter', async function() {
   const mockFeedback = this.createMockFeedback();
-  this.runWithLoadedTree(
-      `
+  const root = await this.runWithLoadedTree(`
     <div role="textbox" contenteditable>Te<br>st</div>
     <button id="go">Go</button>
 
@@ -591,32 +567,29 @@
         sel.modify('extend', 'forward', 'character');
       }, true);
     </script>
-  `,
-      async function(root) {
-        await this.focusFirstTextField(root);
+  `);
+  await this.focusFirstTextField(root);
 
-        const go = root.find({role: RoleType.BUTTON});
-        const moveByChar = go.doDefault.bind(go);
+  const go = root.find({role: RoleType.BUTTON});
+  const moveByChar = go.doDefault.bind(go);
 
-        mockFeedback.call(moveByChar)
-            .expectSpeech('T', 'selected')
-            .call(moveByChar)
-            .expectSpeech('e', 'selected')
-            .call(moveByChar)
-            .expectSpeech('selected')
-            .call(moveByChar)
-            .expectSpeech('s', 'selected')
-            .call(moveByChar)
-            .expectSpeech('t', 'selected')
+  mockFeedback.call(moveByChar)
+      .expectSpeech('T', 'selected')
+      .call(moveByChar)
+      .expectSpeech('e', 'selected')
+      .call(moveByChar)
+      .expectSpeech('selected')
+      .call(moveByChar)
+      .expectSpeech('s', 'selected')
+      .call(moveByChar)
+      .expectSpeech('t', 'selected')
 
-            .replay();
-      });
+      .replay();
 });
 
-TEST_F('ChromeVoxEditingTest', 'RichTextImageByCharacter', function() {
+TEST_F('ChromeVoxEditingTest', 'RichTextImageByCharacter', async function() {
   const mockFeedback = this.createMockFeedback();
-  this.runWithLoadedTree(
-      `
+  const root = await this.runWithLoadedTree(`
     <p contenteditable>
       <img alt="dog"> is a <img alt="cat"> test
     </p>
@@ -635,60 +608,56 @@
         sel.modify('move', dir, 'character');
       }, true);
     </script>
-  `,
-      async function(root) {
-        await this.focusFirstTextField(root, {role: RoleType.PARAGRAPH});
+  `);
+  await this.focusFirstTextField(root, {role: RoleType.PARAGRAPH});
 
-        const go = root.find({role: RoleType.BUTTON});
-        const moveByChar = go.doDefault.bind(go);
-        const lineText = 'dog is a cat test mled';
-        const lineOnCatText = 'dog is a cat img test mled';
+  const go = root.find({role: RoleType.BUTTON});
+  const moveByChar = go.doDefault.bind(go);
+  const lineText = 'dog is a cat test mled';
+  const lineOnCatText = 'dog is a cat img test mled';
 
-        // This is initial output from focusing the contenteditable (which has
-        // no role).
-        mockFeedback.expectSpeech(
-            'dog', 'Image', ' is a ', 'cat', 'Image', ' test');
-        mockFeedback.expectBraille('dog img is a cat img test');
+  // This is initial output from focusing the contenteditable (which has
+  // no role).
+  mockFeedback.expectSpeech('dog', 'Image', ' is a ', 'cat', 'Image', ' test');
+  mockFeedback.expectBraille('dog img is a cat img test');
 
-        const moves = [
-          {speech: [' '], braille: [lineText, {startIndex: 3, endIndex: 3}]},
-          {speech: ['i'], braille: [lineText, {startIndex: 4, endIndex: 4}]},
-          {speech: ['s'], braille: [lineText, {startIndex: 5, endIndex: 5}]},
-          {speech: [' '], braille: [lineText, {startIndex: 6, endIndex: 6}]},
-          {speech: ['a'], braille: [lineText, {startIndex: 7, endIndex: 7}]},
-          {speech: [' '], braille: [lineText, {startIndex: 8, endIndex: 8}]}, {
-            speech: ['cat', 'Image'],
-            braille: [lineOnCatText, {startIndex: 9, endIndex: 9}]
-          },
-          {speech: [' '], braille: [lineText, {startIndex: 12, endIndex: 12}]}
-        ];
+  const moves = [
+    {speech: [' '], braille: [lineText, {startIndex: 3, endIndex: 3}]},
+    {speech: ['i'], braille: [lineText, {startIndex: 4, endIndex: 4}]},
+    {speech: ['s'], braille: [lineText, {startIndex: 5, endIndex: 5}]},
+    {speech: [' '], braille: [lineText, {startIndex: 6, endIndex: 6}]},
+    {speech: ['a'], braille: [lineText, {startIndex: 7, endIndex: 7}]},
+    {speech: [' '], braille: [lineText, {startIndex: 8, endIndex: 8}]}, {
+      speech: ['cat', 'Image'],
+      braille: [lineOnCatText, {startIndex: 9, endIndex: 9}]
+    },
+    {speech: [' '], braille: [lineText, {startIndex: 12, endIndex: 12}]}
+  ];
 
-        for (const item of moves) {
-          mockFeedback.call(moveByChar);
-          mockFeedback.expectSpeech.apply(mockFeedback, item.speech);
-          mockFeedback.expectBraille.apply(mockFeedback, item.braille);
-        }
+  for (const item of moves) {
+    mockFeedback.call(moveByChar);
+    mockFeedback.expectSpeech.apply(mockFeedback, item.speech);
+    mockFeedback.expectBraille.apply(mockFeedback, item.braille);
+  }
 
-        const backMoves = moves.reverse();
-        backMoves.shift();
-        for (const backItem of backMoves) {
-          mockFeedback.call(moveByChar);
-          mockFeedback.expectSpeech.apply(mockFeedback, backItem.speech);
-          mockFeedback.expectBraille.apply(mockFeedback, backItem.braille);
-        }
+  const backMoves = moves.reverse();
+  backMoves.shift();
+  for (const backItem of backMoves) {
+    mockFeedback.call(moveByChar);
+    mockFeedback.expectSpeech.apply(mockFeedback, backItem.speech);
+    mockFeedback.expectBraille.apply(mockFeedback, backItem.braille);
+  }
 
-        mockFeedback.replay();
-      });
+  mockFeedback.replay();
 });
 
-TEST_F('ChromeVoxEditingTest', 'RichTextSelectByLine', function() {
+TEST_F('ChromeVoxEditingTest', 'RichTextSelectByLine', async function() {
   const mockFeedback = this.createMockFeedback();
   // Use digit strings like "11111" and "22222" because the character widths
   // of digits are always the same. This means the test can move down one line
   // middle of "11111" and reliably hit a given character position in "22222",
   // regardless of font configuration. https://crbug.com/898213
-  this.runWithLoadedTree(
-      `
+  const root = await this.runWithLoadedTree(`
     <div>
       <button id="go">Go</button>
     </div>
@@ -724,80 +693,78 @@
         sel.modify.apply(sel, commands.shift());
       }, true);
     </script>
-  `,
-      async function(root) {
-        await this.focusFirstTextField(root, {role: RoleType.PARAGRAPH});
+  `);
+  await this.focusFirstTextField(root, {role: RoleType.PARAGRAPH});
 
-        const go = root.find({role: RoleType.BUTTON});
-        const move = go.doDefault.bind(go);
+  const go = root.find({role: RoleType.BUTTON});
+  const move = go.doDefault.bind(go);
 
-        // By character.
-        mockFeedback.call(move)
-            .expectSpeech('1', 'selected')
-            .expectBraille('11111 line\nmled', {startIndex: 0, endIndex: 1})
-            .call(move)
-            .expectSpeech('1', 'selected')
-            .expectBraille('11111 line\nmled', {startIndex: 0, endIndex: 2})
+  // By character.
+  mockFeedback.call(move)
+      .expectSpeech('1', 'selected')
+      .expectBraille('11111 line\nmled', {startIndex: 0, endIndex: 1})
+      .call(move)
+      .expectSpeech('1', 'selected')
+      .expectBraille('11111 line\nmled', {startIndex: 0, endIndex: 2})
 
-            // Forward selection by line (notice the partial selections from the
-            // first and second lines).
-            .call(move)
-            .expectSpeech('111 line', '22', 'selected')
-            .expectBraille('22222 line\n', {startIndex: 0, endIndex: 2})
+      // Forward selection by line (notice the partial selections from the
+      // first and second lines).
+      .call(move)
+      .expectSpeech('111 line', '22', 'selected')
+      .expectBraille('22222 line\n', {startIndex: 0, endIndex: 2})
 
-            .call(move)
-            .expectSpeech('222 line', '33', 'selected')
-            .expectBraille('33333 line\n', {startIndex: 0, endIndex: 2})
+      .call(move)
+      .expectSpeech('222 line', '33', 'selected')
+      .expectBraille('33333 line\n', {startIndex: 0, endIndex: 2})
 
-            // Backward selection by line.
-            .call(move)
-            .expectSpeech('222 line', '33', 'unselected')
-            .expectBraille('22222 line\n', {startIndex: 0, endIndex: 2})
+      // Backward selection by line.
+      .call(move)
+      .expectSpeech('222 line', '33', 'unselected')
+      .expectBraille('22222 line\n', {startIndex: 0, endIndex: 2})
 
-            .call(move)
-            .expectSpeech('111 line', '22', 'unselected')
-            .expectBraille('11111 line\nmled', {startIndex: 0, endIndex: 2})
+      .call(move)
+      .expectSpeech('111 line', '22', 'unselected')
+      .expectBraille('11111 line\nmled', {startIndex: 0, endIndex: 2})
 
-            // Document boundary.
-            .call(move)
-            .expectSpeech('111 line', '22222 line', '33333 line', 'selected')
-            .expectBraille('33333 line\n', {startIndex: 0, endIndex: 10})
+      // Document boundary.
+      .call(move)
+      .expectSpeech('111 line', '22222 line', '33333 line', 'selected')
+      .expectBraille('33333 line\n', {startIndex: 0, endIndex: 10})
 
-            // The script repositions the caret to the 'n' of the third line.
-            .call(move)
-            .expectSpeech('33333 line')
-            .expectBraille('33333 line\n', {startIndex: 10, endIndex: 10})
-            .call(move)
-            .expectSpeech('e')
-            .expectBraille('33333 line\n', {startIndex: 9, endIndex: 9})
-            .call(move)
-            .expectSpeech('n')
-            .expectBraille('33333 line\n', {startIndex: 8, endIndex: 8})
+      // The script repositions the caret to the 'n' of the third line.
+      .call(move)
+      .expectSpeech('33333 line')
+      .expectBraille('33333 line\n', {startIndex: 10, endIndex: 10})
+      .call(move)
+      .expectSpeech('e')
+      .expectBraille('33333 line\n', {startIndex: 9, endIndex: 9})
+      .call(move)
+      .expectSpeech('n')
+      .expectBraille('33333 line\n', {startIndex: 8, endIndex: 8})
 
-            // Backward selection.
+      // Backward selection.
 
-            // Growing.
-            .call(move)
-            .expectSpeech('ne', '33333 li', 'selected')
-            .expectBraille('22222 line\n', {startIndex: 8, endIndex: 11})
+      // Growing.
+      .call(move)
+      .expectSpeech('ne', '33333 li', 'selected')
+      .expectBraille('22222 line\n', {startIndex: 8, endIndex: 11})
 
-            .call(move)
-            .expectSpeech('ne', '22222 li', 'selected')
-            .expectBraille('11111 line\n', {startIndex: 8, endIndex: 11})
+      .call(move)
+      .expectSpeech('ne', '22222 li', 'selected')
+      .expectBraille('11111 line\n', {startIndex: 8, endIndex: 11})
 
-            // Shrinking.
-            .call(move)
-            .expectSpeech('ne', '22222 li', 'unselected')
-            .expectBraille('22222 line\n', {startIndex: 8, endIndex: 11})
+      // Shrinking.
+      .call(move)
+      .expectSpeech('ne', '22222 li', 'unselected')
+      .expectBraille('22222 line\n', {startIndex: 8, endIndex: 11})
 
-            .replay();
-      });
+      .replay();
 });
 
-TEST_F('ChromeVoxEditingTest', 'RichTextSelectComplexStructure', function() {
-  const mockFeedback = this.createMockFeedback();
-  this.runWithLoadedTree(
-      `
+TEST_F(
+    'ChromeVoxEditingTest', 'RichTextSelectComplexStructure', async function() {
+      const mockFeedback = this.createMockFeedback();
+      const root = await this.runWithLoadedTree(`
     <div>
       <button id="go">Go</button>
     </div>
@@ -833,420 +800,396 @@
         sel.modify.apply(sel, commands.shift());
       }, true);
     </script>
-  `,
-      async function(root) {
-        await this.focusFirstTextField(root, {role: RoleType.TEXT_FIELD});
+  `);
+      await this.focusFirstTextField(root, {role: RoleType.TEXT_FIELD});
 
-        const go = root.find({role: RoleType.BUTTON});
-        const move = go.doDefault.bind(go);
+      const go = root.find({role: RoleType.BUTTON});
+      const move = go.doDefault.bind(go);
 
-        // By character.
-        mockFeedback.call(move)
-            .expectSpeech('1', 'Heading 1', 'selected')
-            .expectBraille('11111 line h1 mled', {startIndex: 0, endIndex: 1})
-            .call(move)
-            .expectSpeech('1', 'Heading 1', 'selected')
-            .expectBraille('11111 line h1 mled', {startIndex: 0, endIndex: 2})
+      // By character.
+      mockFeedback.call(move)
+          .expectSpeech('1', 'Heading 1', 'selected')
+          .expectBraille('11111 line h1 mled', {startIndex: 0, endIndex: 1})
+          .call(move)
+          .expectSpeech('1', 'Heading 1', 'selected')
+          .expectBraille('11111 line h1 mled', {startIndex: 0, endIndex: 2})
 
-            // Forward selection by line (notice the partial selections from the
-            // first and second lines).
-            .call(move)
-            .expectSpeech('111 line', 'Heading 1', '222', 'Link', 'selected')
-            .expectBraille('22222 line lnk', {startIndex: 0, endIndex: 3})
+          // Forward selection by line (notice the partial selections from the
+          // first and second lines).
+          .call(move)
+          .expectSpeech('111 line', 'Heading 1', '222', 'Link', 'selected')
+          .expectBraille('22222 line lnk', {startIndex: 0, endIndex: 3})
 
-            .call(move)
-            .expectSpeech('22 line', 'Link', 'selected')
-            .expectBraille(
-                '33333 line 1. lstitm lst +1', {startIndex: 0, endIndex: 0})
+          .call(move)
+          .expectSpeech('22 line', 'Link', 'selected')
+          .expectBraille(
+              '33333 line 1. lstitm lst +1', {startIndex: 0, endIndex: 0})
 
-            // Shrinking.
-            .call(move)
-            .expectSpeech('22 line', 'Link', 'unselected')
-            .expectBraille('22222 line lnk', {startIndex: 0, endIndex: 3})
+          // Shrinking.
+          .call(move)
+          .expectSpeech('22 line', 'Link', 'unselected')
+          .expectBraille('22222 line lnk', {startIndex: 0, endIndex: 3})
 
-            .call(move)
-            .expectSpeech('111 line', 'Heading 1', '222', 'Link', 'unselected')
-            .expectBraille('11111 line h1 mled', {startIndex: 0, endIndex: 2})
+          .call(move)
+          .expectSpeech('111 line', 'Heading 1', '222', 'Link', 'unselected')
+          .expectBraille('11111 line h1 mled', {startIndex: 0, endIndex: 2})
 
-            // Document boundary.
-            .call(move)
-            .expectSpeech(
-                '111 line', 'Heading 1', '22222 line', 'Link', '33333 line',
-                'List item', 'selected')
-            .expectBraille(
-                '33333 line 1. lstitm lst +1', {startIndex: 0, endIndex: 10})
+          // Document boundary.
+          .call(move)
+          .expectSpeech(
+              '111 line', 'Heading 1', '22222 line', 'Link', '33333 line',
+              'List item', 'selected')
+          .expectBraille(
+              '33333 line 1. lstitm lst +1', {startIndex: 0, endIndex: 10})
 
-            // The script repositions the caret to the end of the last line.
-            .call(move)
-            .expectSpeech('End of text')
-            .expectBraille(
-                '33333 line 1. lstitm lst +1', {startIndex: 10, endIndex: 10})
-            .call(move)
-            .expectSpeech('e')
-            .expectBraille(
-                '33333 line 1. lstitm lst +1', {startIndex: 9, endIndex: 9})
-            .call(move)
-            .expectSpeech('n')
-            .expectBraille(
-                '33333 line 1. lstitm lst +1', {startIndex: 8, endIndex: 8})
+          // The script repositions the caret to the end of the last line.
+          .call(move)
+          .expectSpeech('End of text')
+          .expectBraille(
+              '33333 line 1. lstitm lst +1', {startIndex: 10, endIndex: 10})
+          .call(move)
+          .expectSpeech('e')
+          .expectBraille(
+              '33333 line 1. lstitm lst +1', {startIndex: 9, endIndex: 9})
+          .call(move)
+          .expectSpeech('n')
+          .expectBraille(
+              '33333 line 1. lstitm lst +1', {startIndex: 8, endIndex: 8})
 
-            // Backward selection.
-            // Some bugs exist in Blink where we don't get all selection events
-            // in this complex structure via extending selection, so we do it
-            // twice.
-            .call(move)
-            .call(move)
-            .expectSpeech('ine', 'Link')
-            .expectSpeech('33333 li', 'List item', 'selected')
-            .expectBraille('11111 line h1', {startIndex: 7, endIndex: 10})
+          // Backward selection.
+          // Some bugs exist in Blink where we don't get all selection events
+          // in this complex structure via extending selection, so we do it
+          // twice.
+          .call(move)
+          .call(move)
+          .expectSpeech('ine', 'Link')
+          .expectSpeech('33333 li', 'List item', 'selected')
+          .expectBraille('11111 line h1', {startIndex: 7, endIndex: 10})
 
-            .replay();
-      });
-});
+          .replay();
+    });
 
-TEST_F('ChromeVoxEditingTest', 'EditableLineOneStaticText', function() {
-  this.runWithLoadedTree(
-      `
+TEST_F('ChromeVoxEditingTest', 'EditableLineOneStaticText', async function() {
+  const root = await this.runWithLoadedTree(`
     <p contenteditable style="word-spacing:100000px">this is a test</p>
-  `,
-      function(root) {
-        const staticText = root.find({role: RoleType.STATIC_TEXT});
+  `);
+  const staticText = root.find({role: RoleType.STATIC_TEXT});
 
-        let e = new EditableLine(staticText, 0, staticText, 0);
-        assertEquals('this ', e.text);
+  let e = new EditableLine(staticText, 0, staticText, 0);
+  assertEquals('this ', e.text);
 
-        assertEquals(0, e.startOffset);
-        assertEquals(0, e.endOffset);
-        assertEquals(0, e.localStartOffset);
-        assertEquals(0, e.localEndOffset);
+  assertEquals(0, e.startOffset);
+  assertEquals(0, e.endOffset);
+  assertEquals(0, e.localStartOffset);
+  assertEquals(0, e.localEndOffset);
 
-        assertEquals(0, e.containerStartOffset);
-        assertEquals(4, e.containerEndOffset);
+  assertEquals(0, e.containerStartOffset);
+  assertEquals(4, e.containerEndOffset);
 
-        e = new EditableLine(staticText, 1, staticText, 1);
-        assertEquals('this ', e.text);
+  e = new EditableLine(staticText, 1, staticText, 1);
+  assertEquals('this ', e.text);
 
-        assertEquals(1, e.startOffset);
-        assertEquals(1, e.endOffset);
-        assertEquals(1, e.localStartOffset);
-        assertEquals(1, e.localEndOffset);
+  assertEquals(1, e.startOffset);
+  assertEquals(1, e.endOffset);
+  assertEquals(1, e.localStartOffset);
+  assertEquals(1, e.localEndOffset);
 
-        assertEquals(0, e.containerStartOffset);
-        assertEquals(4, e.containerEndOffset);
+  assertEquals(0, e.containerStartOffset);
+  assertEquals(4, e.containerEndOffset);
 
-        e = new EditableLine(staticText, 5, staticText, 5);
-        assertEquals('is ', e.text);
+  e = new EditableLine(staticText, 5, staticText, 5);
+  assertEquals('is ', e.text);
 
-        assertEquals(0, e.startOffset);
-        assertEquals(0, e.endOffset);
-        assertEquals(5, e.localStartOffset);
-        assertEquals(5, e.localEndOffset);
+  assertEquals(0, e.startOffset);
+  assertEquals(0, e.endOffset);
+  assertEquals(5, e.localStartOffset);
+  assertEquals(5, e.localEndOffset);
 
-        assertEquals(0, e.containerStartOffset);
-        assertEquals(2, e.containerEndOffset);
+  assertEquals(0, e.containerStartOffset);
+  assertEquals(2, e.containerEndOffset);
 
-        e = new EditableLine(staticText, 7, staticText, 7);
-        assertEquals('is ', e.text);
+  e = new EditableLine(staticText, 7, staticText, 7);
+  assertEquals('is ', e.text);
 
-        assertEquals(2, e.startOffset);
-        assertEquals(2, e.endOffset);
-        assertEquals(7, e.localStartOffset);
-        assertEquals(7, e.localEndOffset);
+  assertEquals(2, e.startOffset);
+  assertEquals(2, e.endOffset);
+  assertEquals(7, e.localStartOffset);
+  assertEquals(7, e.localEndOffset);
 
-        assertEquals(0, e.containerStartOffset);
-        assertEquals(2, e.containerEndOffset);
-      });
+  assertEquals(0, e.containerStartOffset);
+  assertEquals(2, e.containerEndOffset);
 });
 
-TEST_F('ChromeVoxEditingTest', 'EditableLineTwoStaticTexts', function() {
-  this.runWithLoadedTree(
-      `
+TEST_F('ChromeVoxEditingTest', 'EditableLineTwoStaticTexts', async function() {
+  const root = await this.runWithLoadedTree(`
     <p contenteditable>hello <b>world</b></p>
-  `,
-      function(root) {
-        const text = root.find({role: RoleType.STATIC_TEXT});
-        const bold = text.nextSibling;
+  `);
+  const text = root.find({role: RoleType.STATIC_TEXT});
+  const bold = text.nextSibling;
 
-        let e = new EditableLine(text, 0, text, 0);
-        assertEquals('hello world', e.text);
+  let e = new EditableLine(text, 0, text, 0);
+  assertEquals('hello world', e.text);
 
-        assertEquals(0, e.startOffset);
-        assertEquals(0, e.endOffset);
-        assertEquals(0, e.localStartOffset);
-        assertEquals(0, e.localEndOffset);
+  assertEquals(0, e.startOffset);
+  assertEquals(0, e.endOffset);
+  assertEquals(0, e.localStartOffset);
+  assertEquals(0, e.localEndOffset);
 
-        assertEquals(0, e.containerStartOffset);
-        assertEquals(5, e.containerEndOffset);
+  assertEquals(0, e.containerStartOffset);
+  assertEquals(5, e.containerEndOffset);
 
-        e = new EditableLine(text, 5, text, 5);
-        assertEquals('hello world', e.text);
+  e = new EditableLine(text, 5, text, 5);
+  assertEquals('hello world', e.text);
 
-        assertEquals(5, e.startOffset);
-        assertEquals(5, e.endOffset);
-        assertEquals(5, e.localStartOffset);
-        assertEquals(5, e.localEndOffset);
+  assertEquals(5, e.startOffset);
+  assertEquals(5, e.endOffset);
+  assertEquals(5, e.localStartOffset);
+  assertEquals(5, e.localEndOffset);
 
-        assertEquals(0, e.containerStartOffset);
-        assertEquals(5, e.containerEndOffset);
+  assertEquals(0, e.containerStartOffset);
+  assertEquals(5, e.containerEndOffset);
 
-        e = new EditableLine(bold, 0, bold, 0);
-        assertEquals('hello world', e.text);
+  e = new EditableLine(bold, 0, bold, 0);
+  assertEquals('hello world', e.text);
 
-        assertEquals(6, e.startOffset);
-        assertEquals(6, e.endOffset);
-        assertEquals(0, e.localStartOffset);
-        assertEquals(0, e.localEndOffset);
+  assertEquals(6, e.startOffset);
+  assertEquals(6, e.endOffset);
+  assertEquals(0, e.localStartOffset);
+  assertEquals(0, e.localEndOffset);
 
-        assertEquals(6, e.containerStartOffset);
-        assertEquals(10, e.containerEndOffset);
+  assertEquals(6, e.containerStartOffset);
+  assertEquals(10, e.containerEndOffset);
 
-        e = new EditableLine(bold, 4, bold, 4);
-        assertEquals('hello world', e.text);
+  e = new EditableLine(bold, 4, bold, 4);
+  assertEquals('hello world', e.text);
 
-        assertEquals(10, e.startOffset);
-        assertEquals(10, e.endOffset);
-        assertEquals(4, e.localStartOffset);
-        assertEquals(4, e.localEndOffset);
+  assertEquals(10, e.startOffset);
+  assertEquals(10, e.endOffset);
+  assertEquals(4, e.localStartOffset);
+  assertEquals(4, e.localEndOffset);
 
-        assertEquals(6, e.containerStartOffset);
-        assertEquals(10, e.containerEndOffset);
-      });
+  assertEquals(6, e.containerStartOffset);
+  assertEquals(10, e.containerEndOffset);
 });
 
-TEST_F('ChromeVoxEditingTest', 'EditableLineEquality', function() {
-  this.runWithLoadedTree(
-      `
+TEST_F('ChromeVoxEditingTest', 'EditableLineEquality', async function() {
+  const root = await this.runWithLoadedTree(`
     <div contenteditable role="textbox">
       <p style="word-spacing:100000px">this is a test</p>
       <p>hello <b>world</b></p>
     </div>
-  `,
-      function(root) {
-        const thisIsATest =
-            root.findAll({role: RoleType.PARAGRAPH})[0].firstChild;
-        const hello = root.findAll({role: RoleType.PARAGRAPH})[1].firstChild;
-        const world = root.findAll({role: RoleType.PARAGRAPH})[1].lastChild;
+  `);
+  const thisIsATest = root.findAll({role: RoleType.PARAGRAPH})[0].firstChild;
+  const hello = root.findAll({role: RoleType.PARAGRAPH})[1].firstChild;
+  const world = root.findAll({role: RoleType.PARAGRAPH})[1].lastChild;
 
-        // The same position -- sanity check.
-        let e1 = new EditableLine(thisIsATest, 0, thisIsATest, 0);
-        assertEquals('this ', e1.text);
-        assertTrue(e1.isSameLine(e1));
+  // The same position -- sanity check.
+  let e1 = new EditableLine(thisIsATest, 0, thisIsATest, 0);
+  assertEquals('this ', e1.text);
+  assertTrue(e1.isSameLine(e1));
 
-        // Offset into the same soft line.
-        let e2 = new EditableLine(thisIsATest, 1, thisIsATest, 1);
-        assertTrue(e1.isSameLine(e2));
+  // Offset into the same soft line.
+  let e2 = new EditableLine(thisIsATest, 1, thisIsATest, 1);
+  assertTrue(e1.isSameLine(e2));
 
-        // Boundary.
-        e2 = new EditableLine(thisIsATest, 4, thisIsATest, 4);
-        assertTrue(e1.isSameLine(e2));
+  // Boundary.
+  e2 = new EditableLine(thisIsATest, 4, thisIsATest, 4);
+  assertTrue(e1.isSameLine(e2));
 
-        // Offsets into different soft lines.
-        e2 = new EditableLine(thisIsATest, 5, thisIsATest, 5);
-        assertEquals('is ', e2.text);
-        assertFalse(e1.isSameLine(e2));
+  // Offsets into different soft lines.
+  e2 = new EditableLine(thisIsATest, 5, thisIsATest, 5);
+  assertEquals('is ', e2.text);
+  assertFalse(e1.isSameLine(e2));
 
-        // Sanity check; second soft line.
-        assertTrue(e2.isSameLine(e2));
+  // Different offsets into second soft line.
+  e1 = new EditableLine(thisIsATest, 6, thisIsATest, 6);
+  assertTrue(e1.isSameLine(e2));
 
-        // Different offsets into second soft line.
-        e1 = new EditableLine(thisIsATest, 6, thisIsATest, 6);
-        assertTrue(e1.isSameLine(e2));
+  // Boundary.
+  e1 = new EditableLine(thisIsATest, 7, thisIsATest, 7);
+  assertTrue(e1.isSameLine(e2));
 
-        // Boundary.
-        e1 = new EditableLine(thisIsATest, 7, thisIsATest, 7);
-        assertTrue(e1.isSameLine(e2));
+  // Third line.
+  e1 = new EditableLine(thisIsATest, 8, thisIsATest, 8);
+  assertEquals('a ', e1.text);
+  assertFalse(e1.isSameLine(e2));
 
-        // Third line.
-        e1 = new EditableLine(thisIsATest, 8, thisIsATest, 8);
-        assertEquals('a ', e1.text);
-        assertFalse(e1.isSameLine(e2));
+  // Last line.
+  e2 = new EditableLine(thisIsATest, 10, thisIsATest, 10);
+  assertEquals('test', e2.text);
+  assertFalse(e1.isSameLine(e2));
 
-        // Last line.
-        e2 = new EditableLine(thisIsATest, 10, thisIsATest, 10);
-        assertEquals('test', e2.text);
-        assertFalse(e1.isSameLine(e2));
+  // Boundary.
+  e1 = new EditableLine(thisIsATest, 13, thisIsATest, 13);
+  assertTrue(e1.isSameLine(e2));
 
-        // Boundary.
-        e1 = new EditableLine(thisIsATest, 13, thisIsATest, 13);
-        assertTrue(e1.isSameLine(e2));
+  // Cross into new paragraph.
+  e2 = new EditableLine(hello, 0, hello, 0);
+  assertEquals('hello world', e2.text);
+  assertFalse(e1.isSameLine(e2));
 
-        // Cross into new paragraph.
-        e2 = new EditableLine(hello, 0, hello, 0);
-        assertEquals('hello world', e2.text);
-        assertFalse(e1.isSameLine(e2));
+  // On same node, with multi-static text line.
+  e1 = new EditableLine(hello, 1, hello, 1);
+  assertTrue(e1.isSameLine(e2));
 
-        // On same node, with multi-static text line.
-        e1 = new EditableLine(hello, 1, hello, 1);
-        assertTrue(e1.isSameLine(e2));
+  // On same node, with multi-static text line; boundary.
+  e1 = new EditableLine(hello, 5, hello, 5);
+  assertTrue(e1.isSameLine(e2));
 
-        // On same node, with multi-static text line; boundary.
-        e1 = new EditableLine(hello, 5, hello, 5);
-        assertTrue(e1.isSameLine(e2));
+  // On different node, with multi-static text line.
+  e1 = new EditableLine(world, 1, world, 1);
+  assertTrue(e1.isSameLine(e2));
 
-        // On different node, with multi-static text line.
-        e1 = new EditableLine(world, 1, world, 1);
-        assertTrue(e1.isSameLine(e2));
-
-        // Another mix of lines.
-        e2 = new EditableLine(thisIsATest, 9, thisIsATest, 9);
-        assertFalse(e1.isSameLine(e2));
-      });
+  // Another mix of lines.
+  e2 = new EditableLine(thisIsATest, 9, thisIsATest, 9);
+  assertFalse(e1.isSameLine(e2));
 });
 
-TEST_F('ChromeVoxEditingTest', 'EditableLineStrictEquality', function() {
-  this.runWithLoadedTree(
-      `
+TEST_F('ChromeVoxEditingTest', 'EditableLineStrictEquality', async function() {
+  const root = await this.runWithLoadedTree(`
     <div contenteditable role="textbox">
       <p style="word-spacing:100000px">this is a test</p>
       <p>hello <b>world</b></p>
     </div>
-  `,
-      function(root) {
-        const thisIsATest =
-            root.findAll({role: RoleType.PARAGRAPH})[0].firstChild;
-        const hello = root.findAll({role: RoleType.PARAGRAPH})[1].firstChild;
-        const world = root.findAll({role: RoleType.PARAGRAPH})[1].lastChild;
+  `);
+  const thisIsATest = root.findAll({role: RoleType.PARAGRAPH})[0].firstChild;
+  const hello = root.findAll({role: RoleType.PARAGRAPH})[1].firstChild;
+  const world = root.findAll({role: RoleType.PARAGRAPH})[1].lastChild;
 
-        // The same position -- sanity check.
-        let e1 = new EditableLine(thisIsATest, 0, thisIsATest, 0);
-        assertEquals('this ', e1.text);
-        assertTrue(e1.isSameLineAndSelection(e1));
+  // The same position -- sanity check.
+  let e1 = new EditableLine(thisIsATest, 0, thisIsATest, 0);
+  assertEquals('this ', e1.text);
+  assertTrue(e1.isSameLineAndSelection(e1));
 
-        // Offset into the same soft line.
-        let e2 = new EditableLine(thisIsATest, 1, thisIsATest, 1);
-        assertFalse(e1.isSameLineAndSelection(e2));
+  // Offset into the same soft line.
+  let e2 = new EditableLine(thisIsATest, 1, thisIsATest, 1);
+  assertFalse(e1.isSameLineAndSelection(e2));
 
-        // Boundary.
-        e2 = new EditableLine(thisIsATest, 4, thisIsATest, 4);
-        assertFalse(e1.isSameLineAndSelection(e2));
+  // Boundary.
+  e2 = new EditableLine(thisIsATest, 4, thisIsATest, 4);
+  assertFalse(e1.isSameLineAndSelection(e2));
 
-        // Offsets into different soft lines.
-        e2 = new EditableLine(thisIsATest, 5, thisIsATest, 5);
-        assertEquals('is ', e2.text);
-        assertFalse(e1.isSameLineAndSelection(e2));
+  // Offsets into different soft lines.
+  e2 = new EditableLine(thisIsATest, 5, thisIsATest, 5);
+  assertEquals('is ', e2.text);
+  assertFalse(e1.isSameLineAndSelection(e2));
 
-        // Sanity check; second soft line.
-        assertTrue(e2.isSameLineAndSelection(e2));
+  // Sanity check; second soft line.
+  assertTrue(e2.isSameLineAndSelection(e2));
 
-        // Different offsets into second soft line.
-        e1 = new EditableLine(thisIsATest, 6, thisIsATest, 6);
-        assertFalse(e1.isSameLineAndSelection(e2));
+  // Different offsets into second soft line.
+  e1 = new EditableLine(thisIsATest, 6, thisIsATest, 6);
+  assertFalse(e1.isSameLineAndSelection(e2));
 
-        // Boundary.
-        e1 = new EditableLine(thisIsATest, 7, thisIsATest, 7);
-        assertFalse(e1.isSameLineAndSelection(e2));
+  // Boundary.
+  e1 = new EditableLine(thisIsATest, 7, thisIsATest, 7);
+  assertFalse(e1.isSameLineAndSelection(e2));
 
-        // Cross into new paragraph.
-        e2 = new EditableLine(hello, 0, hello, 0);
-        assertEquals('hello world', e2.text);
-        assertFalse(e1.isSameLineAndSelection(e2));
+  // Cross into new paragraph.
+  e2 = new EditableLine(hello, 0, hello, 0);
+  assertEquals('hello world', e2.text);
+  assertFalse(e1.isSameLineAndSelection(e2));
 
-        // On same node, with multi-static text line.
-        e1 = new EditableLine(hello, 1, hello, 1);
-        assertFalse(e1.isSameLineAndSelection(e2));
+  // On same node, with multi-static text line.
+  e1 = new EditableLine(hello, 1, hello, 1);
+  assertFalse(e1.isSameLineAndSelection(e2));
 
-        // On same node, with multi-static text line; boundary.
-        e1 = new EditableLine(hello, 5, hello, 5);
-        assertFalse(e1.isSameLineAndSelection(e2));
-      });
+  // On same node, with multi-static text line; boundary.
+  e1 = new EditableLine(hello, 5, hello, 5);
+  assertFalse(e1.isSameLineAndSelection(e2));
 });
 
-TEST_F('ChromeVoxEditingTest', 'EditableLineBaseLineAnchorOrFocus', function() {
-  this.runWithLoadedTree(
-      `
+TEST_F(
+    'ChromeVoxEditingTest', 'EditableLineBaseLineAnchorOrFocus',
+    async function() {
+      const root = await this.runWithLoadedTree(`
     <div contenteditable role="textbox">
       <p style="word-spacing:100000px">this is a test</p>
       <p>hello <b>world</b></p>
     </div>
-  `,
-      function(root) {
-        const thisIsATest =
-            root.findAll({role: RoleType.PARAGRAPH})[0].firstChild;
-        const hello = root.findAll({role: RoleType.PARAGRAPH})[1].firstChild;
-        const world = root.findAll({role: RoleType.PARAGRAPH})[1].lastChild;
+  `);
+      const thisIsATest =
+          root.findAll({role: RoleType.PARAGRAPH})[0].firstChild;
+      const hello = root.findAll({role: RoleType.PARAGRAPH})[1].firstChild;
+      const world = root.findAll({role: RoleType.PARAGRAPH})[1].lastChild;
 
-        // The same position -- sanity check.
-        let e1 = new EditableLine(thisIsATest, 0, thisIsATest, 0, true);
-        assertEquals('this ', e1.text);
+      // The same position -- sanity check.
+      let e1 = new EditableLine(thisIsATest, 0, thisIsATest, 0, true);
+      assertEquals('this ', e1.text);
 
-        // Offsets into different soft lines; base on focus (default).
-        e1 = new EditableLine(thisIsATest, 0, thisIsATest, 6);
-        assertEquals('is ', e1.text);
-        // Notice that the offset is truncated at the beginning of the line.
-        assertEquals(0, e1.startOffset);
-        // Notice that the end offset is properly retained.
-        assertEquals(1, e1.endOffset);
+      // Offsets into different soft lines; base on focus (default).
+      e1 = new EditableLine(thisIsATest, 0, thisIsATest, 6);
+      assertEquals('is ', e1.text);
+      // Notice that the offset is truncated at the beginning of the line.
+      assertEquals(0, e1.startOffset);
+      // Notice that the end offset is properly retained.
+      assertEquals(1, e1.endOffset);
 
-        // Offsets into different soft lines; base on anchor.
-        e1 = new EditableLine(thisIsATest, 0, thisIsATest, 6, true);
-        assertEquals('this ', e1.text);
-        assertEquals(0, e1.startOffset);
-        // Notice            that the end offset is truncated up to the end of
-        // line.
-        assertEquals(5, e1.endOffset);
+      // Offsets into different soft lines; base on anchor.
+      e1 = new EditableLine(thisIsATest, 0, thisIsATest, 6, true);
+      assertEquals('this ', e1.text);
+      assertEquals(0, e1.startOffset);
+      // Notice            that the end offset is truncated up to the end of
+      // line.
+      assertEquals(5, e1.endOffset);
 
-        // Across paragraph selection with base line on focus.
-        e1 = new EditableLine(thisIsATest, 5, hello, 2);
-        assertEquals('hello world', e1.text);
-        assertEquals(0, e1.startOffset);
-        assertEquals(2, e1.endOffset);
+      // Across paragraph selection with base line on focus.
+      e1 = new EditableLine(thisIsATest, 5, hello, 2);
+      assertEquals('hello world', e1.text);
+      assertEquals(0, e1.startOffset);
+      assertEquals(2, e1.endOffset);
 
-        // Across paragraph selection with base line on anchor.
-        e1 = new EditableLine(thisIsATest, 5, hello, 2, true);
-        assertEquals('is ', e1.text);
-        assertEquals(0, e1.startOffset);
-        assertEquals(3, e1.endOffset);
-      });
-});
+      // Across paragraph selection with base line on anchor.
+      e1 = new EditableLine(thisIsATest, 5, hello, 2, true);
+      assertEquals('is ', e1.text);
+      assertEquals(0, e1.startOffset);
+      assertEquals(3, e1.endOffset);
+    });
 
-TEST_F('ChromeVoxEditingTest', 'IsValidLine', function() {
-  this.runWithLoadedTree(
-      `
+TEST_F('ChromeVoxEditingTest', 'IsValidLine', async function() {
+  const root = await this.runWithLoadedTree(`
     <div contenteditable role="textbox">
       <p style="word-spacing:100000px">this is a test</p>
       <p>end</p>
     </div>
-  `,
-      function(root) {
-        // Each word is on its own line, but parented by a static text.
-        const [text, endText] = root.findAll({role: RoleType.STATIC_TEXT});
+  `);
+  // Each word is on its own line, but parented by a static text.
+  const [text, endText] = root.findAll({role: RoleType.STATIC_TEXT});
 
-        // The EditableLine object automatically adjusts to surround the line no
-        // matter what the input is.
-        const line = new EditableLine(text, 0, text, 0);
-        assertTrue(line.isValidLine());
+  // The EditableLine object automatically adjusts to surround the line no
+  // matter what the input is.
+  const line = new EditableLine(text, 0, text, 0);
+  assertTrue(line.isValidLine());
 
-        // During the course of editing operations, this line may become
-        // invalidted. For example, if a user starts typing into the line, the
-        // bounding nodes might change.
-        // Simulate that here by modifying private state.
+  // During the course of editing operations, this line may become
+  // invalidted. For example, if a user starts typing into the line, the
+  // bounding nodes might change.
+  // Simulate that here by modifying private state.
 
-        // This puts the line at offset 8 (|this is a|).
-        line.localLineStartContainerOffset_ = 0;
-        line.localLineEndContainerOffset_ = 8;
-        assertFalse(line.isValidLine());
+  // This puts the line at offset 8 (|this is a|).
+  line.localLineStartContainerOffset_ = 0;
+  line.localLineEndContainerOffset_ = 8;
+  assertFalse(line.isValidLine());
 
-        // This puts us in the first line.
-        line.localLineStartContainerOffset_ = 0;
-        line.localLineEndContainerOffset_ = 4;
-        assertTrue(line.isValidLine());
+  // This puts us in the first line.
+  line.localLineStartContainerOffset_ = 0;
+  line.localLineEndContainerOffset_ = 4;
+  assertTrue(line.isValidLine());
 
-        // This is still fine (for our purposes) because the line is still
-        // intact.
-        line.localLineStartContainerOffset_ = 0;
-        line.localLineEndContainerOffset_ = 2;
-        assertTrue(line.isValidLine());
+  // This is still fine (for our purposes) because the line is still
+  // intact.
+  line.localLineStartContainerOffset_ = 0;
+  line.localLineEndContainerOffset_ = 2;
+  assertTrue(line.isValidLine());
 
-        // The line has changed. The end has been moved for some reason.
-        line.lineEndContainer_ = endText;
-        assertFalse(line.isValidLine());
-      });
+  // The line has changed. The end has been moved for some reason.
+  line.lineEndContainer_ = endText;
+  assertFalse(line.isValidLine());
 });
 
-TEST_F('ChromeVoxEditingTest', 'TelTrimsWhitespace', function() {
+TEST_F('ChromeVoxEditingTest', 'TelTrimsWhitespace', async function() {
   const mockFeedback = this.createMockFeedback();
-  this.runWithLoadedTree(
-      `
+  const root = await this.runWithLoadedTree(`
     <div id="go"></div>
     <input id="input" type="tel"></input>
     <script>
@@ -1266,63 +1209,58 @@
         input.selectionEnd = index;
       }, true);
     </script>
-  `,
-      async function(root) {
-        await this.focusFirstTextField(root);
+  `);
+  await this.focusFirstTextField(root);
 
-        const go = root.find({role: RoleType.GENERIC_CONTAINER});
-        const enterKey = go.doDefault.bind(go);
+  const go = root.find({role: RoleType.GENERIC_CONTAINER});
+  const enterKey = go.doDefault.bind(go);
 
-        mockFeedback.call(enterKey)
-            .expectSpeech('6')
-            .call(enterKey)
-            .expectSpeech('0')
-            .call(enterKey)
-            .expectSpeech('1')
+  mockFeedback.call(enterKey)
+      .expectSpeech('6')
+      .call(enterKey)
+      .expectSpeech('0')
+      .call(enterKey)
+      .expectSpeech('1')
 
-            // Deletion.
-            .call(enterKey)
-            .expectSpeech('1')
-            .replay();
-      });
+      // Deletion.
+      .call(enterKey)
+      .expectSpeech('1')
+      .replay();
 });
 
-TEST_F('ChromeVoxEditingTest', 'BackwardWordDelete', function() {
+TEST_F('ChromeVoxEditingTest', 'BackwardWordDelete', async function() {
   const mockFeedback = this.createMockFeedback();
-  this.runWithLoadedTree(
-      `
+  const root = await this.runWithLoadedTree(`
     <div
         style='max-width: 5px; overflow-wrap: normal'
         contenteditable>
       this is a test
     </div>
-  `,
-      async function(root) {
-        await this.focusFirstTextField(
-            root, {attributes: {nonAtomicTextFieldRoot: true}});
+  `);
+  await this.focusFirstTextField(
+      root, {attributes: {nonAtomicTextFieldRoot: true}});
 
-        mockFeedback.call(this.press(KeyCode.END, {ctrl: true}))
-            .expectSpeech('test')
-            .call(this.press(KeyCode.BACK, {ctrl: true}))
-            .expectSpeech('test, deleted')
-            .expectBraille('a\u00a0', {startIndex: 2, endIndex: 2})
-            .call(this.press(KeyCode.BACK, {ctrl: true}))
-            .expectSpeech('a , deleted')
-            .expectBraille('is\u00a0', {startIndex: 3, endIndex: 3})
-            .call(this.press(KeyCode.BACK, {ctrl: true}))
-            .expectSpeech('is , deleted')
-            .expectBraille('this\u00a0mled', {startIndex: 5, endIndex: 5})
-            .call(this.press(KeyCode.BACK, {ctrl: true}))
-            .expectBraille(' mled', {startIndex: 0, endIndex: 0})
-            .replay();
-      });
+  mockFeedback.call(this.press(KeyCode.END, {ctrl: true}))
+      .expectSpeech('test')
+      .call(this.press(KeyCode.BACK, {ctrl: true}))
+      .expectSpeech('test, deleted')
+      .expectBraille('a\u00a0', {startIndex: 2, endIndex: 2})
+      .call(this.press(KeyCode.BACK, {ctrl: true}))
+      .expectSpeech('a , deleted')
+      .expectBraille('is\u00a0', {startIndex: 3, endIndex: 3})
+      .call(this.press(KeyCode.BACK, {ctrl: true}))
+      .expectSpeech('is , deleted')
+      .expectBraille('this\u00a0mled', {startIndex: 5, endIndex: 5})
+      .call(this.press(KeyCode.BACK, {ctrl: true}))
+      .expectBraille(' mled', {startIndex: 0, endIndex: 0})
+      .replay();
 });
 
 TEST_F(
-    'ChromeVoxEditingTest', 'BackwardWordDeleteAcrossParagraphs', function() {
+    'ChromeVoxEditingTest', 'BackwardWordDeleteAcrossParagraphs',
+    async function() {
       const mockFeedback = this.createMockFeedback();
-      this.runWithLoadedTree(
-          `
+      const root = await this.runWithLoadedTree(`
     <div
         style='max-width: 5px; overflow-wrap: normal'
         contenteditable
@@ -1330,30 +1268,27 @@
       <p>first line</p>
       <p>second line</p>
     </div>
-  `,
-          async function(root) {
-            await this.focusFirstTextField(root);
+  `);
+      await this.focusFirstTextField(root);
 
-            mockFeedback.call(this.press(KeyCode.END, {ctrl: true}))
-                .expectSpeech('line')
-                .call(this.press(KeyCode.BACK, {ctrl: true}))
-                .expectSpeech('line, deleted')
-                .call(this.press(KeyCode.BACK, {ctrl: true}))
-                .expectSpeech('second , deleted')
-                .call(this.press(KeyCode.BACK, {ctrl: true}))
-                .expectSpeech('line')
-                .call(this.press(KeyCode.BACK, {ctrl: true}))
-                .expectSpeech('line, deleted')
-                .call(this.press(KeyCode.BACK, {ctrl: true}))
-                .expectSpeech('first , deleted')
-                .replay();
-          });
+      mockFeedback.call(this.press(KeyCode.END, {ctrl: true}))
+          .expectSpeech('line')
+          .call(this.press(KeyCode.BACK, {ctrl: true}))
+          .expectSpeech('line, deleted')
+          .call(this.press(KeyCode.BACK, {ctrl: true}))
+          .expectSpeech('second , deleted')
+          .call(this.press(KeyCode.BACK, {ctrl: true}))
+          .expectSpeech('line')
+          .call(this.press(KeyCode.BACK, {ctrl: true}))
+          .expectSpeech('line, deleted')
+          .call(this.press(KeyCode.BACK, {ctrl: true}))
+          .expectSpeech('first , deleted')
+          .replay();
     });
 
-TEST_F('ChromeVoxEditingTest', 'GrammarErrors', function() {
+TEST_F('ChromeVoxEditingTest', 'GrammarErrors', async function() {
   const mockFeedback = this.createMockFeedback();
-  this.runWithLoadedTree(
-      `
+  const root = await this.runWithLoadedTree(`
     <div contenteditable="true" role="textbox">
       This <span aria-invalid="grammar">are</span> a test
     </div>
@@ -1365,269 +1300,247 @@
         sel.modify('move', 'forward', 'character');
       }, true);
     </script>
-  `,
-      async function(root) {
-        await this.focusFirstTextField(root);
+  `);
+  await this.focusFirstTextField(root);
 
-        const go = root.find({role: RoleType.BUTTON});
-        const moveByChar = go.doDefault.bind(go);
+  const go = root.find({role: RoleType.BUTTON});
+  const moveByChar = go.doDefault.bind(go);
 
-        mockFeedback.call(moveByChar)
-            .expectSpeech('h')
-            .call(moveByChar)
-            .expectSpeech('i')
-            .call(moveByChar)
-            .expectSpeech('s')
-            .call(moveByChar)
-            .expectSpeech(' ')
+  mockFeedback.call(moveByChar)
+      .expectSpeech('h')
+      .call(moveByChar)
+      .expectSpeech('i')
+      .call(moveByChar)
+      .expectSpeech('s')
+      .call(moveByChar)
+      .expectSpeech(' ')
 
-            .call(moveByChar)
-            .expectSpeech('a', 'Grammar error')
-            .call(moveByChar)
-            .expectSpeech('r')
-            .call(moveByChar)
-            .expectSpeech('e')
-            .call(moveByChar)
-            .expectSpeech(' ', 'Leaving grammar error')
+      .call(moveByChar)
+      .expectSpeech('a', 'Grammar error')
+      .call(moveByChar)
+      .expectSpeech('r')
+      .call(moveByChar)
+      .expectSpeech('e')
+      .call(moveByChar)
+      .expectSpeech(' ', 'Leaving grammar error')
 
-            .replay();
-      });
+      .replay();
 });
 
 // Flaky test, crbug.com/1098642.
 TEST_F(
-    'ChromeVoxEditingTest', 'DISABLED_CharacterTypedAfterNewLine', function() {
+    'ChromeVoxEditingTest', 'DISABLED_CharacterTypedAfterNewLine',
+    async function() {
       const mockFeedback = this.createMockFeedback();
-      this.runWithLoadedTree(
-          `
+      const root = await this.runWithLoadedTree(`
     <p>start</p>
     <div contenteditable role="textbox">
       <p>hello</p>
     </div>
-  `,
-          async function(root) {
-            await this.focusFirstTextField(root);
+  `);
+      await this.focusFirstTextField(root);
 
-            mockFeedback.call(this.press(KeyCode.END, {ctrl: true}))
-                .expectSpeech('hello')
-                .call(this.press(KeyCode.RETURN))
-                .expectSpeech('\n')
-                .call(this.press(KeyCode.A))
-                .expectSpeech('a')
-                .replay();
-          });
+      mockFeedback.call(this.press(KeyCode.END, {ctrl: true}))
+          .expectSpeech('hello')
+          .call(this.press(KeyCode.RETURN))
+          .expectSpeech('\n')
+          .call(this.press(KeyCode.A))
+          .expectSpeech('a')
+          .replay();
     });
 
-TEST_F('ChromeVoxEditingTest', 'SelectAll', function() {
+TEST_F('ChromeVoxEditingTest', 'SelectAll', async function() {
   const mockFeedback = this.createMockFeedback();
-  this.runWithLoadedTree(
-      `
+  const root = await this.runWithLoadedTree(`
     <div contenteditable role="textbox">
       <p>first line</p>
       <p>second line</p>
       <p>third line</p>
     </div>
-  `,
-      async function(root) {
-        await this.focusFirstTextField(root);
+  `);
+  await this.focusFirstTextField(root);
 
-        mockFeedback.call(this.press(KeyCode.END, {ctrl: true}))
-            .expectSpeech('third line')
-            .call(this.press(KeyCode.A, {ctrl: true}))
-            .expectSpeech('first line', 'second line', 'third line', 'selected')
-            .call(this.press(KeyCode.UP))
-            .expectSpeech('second line')
-            .call(this.press(KeyCode.A, {ctrl: true}))
-            .expectSpeech('first line', 'second line', 'third line', 'selected')
-            .call(this.press(KeyCode.HOME, {ctrl: true}))
-            .expectSpeech('first line')
-            .call(this.press(KeyCode.A, {ctrl: true}))
-            .expectSpeech('first line', 'second line', 'third line', 'selected')
-            .replay();
-      });
+  mockFeedback.call(this.press(KeyCode.END, {ctrl: true}))
+      .expectSpeech('third line')
+      .call(this.press(KeyCode.A, {ctrl: true}))
+      .expectSpeech('first line', 'second line', 'third line', 'selected')
+      .call(this.press(KeyCode.UP))
+      .expectSpeech('second line')
+      .call(this.press(KeyCode.A, {ctrl: true}))
+      .expectSpeech('first line', 'second line', 'third line', 'selected')
+      .call(this.press(KeyCode.HOME, {ctrl: true}))
+      .expectSpeech('first line')
+      .call(this.press(KeyCode.A, {ctrl: true}))
+      .expectSpeech('first line', 'second line', 'third line', 'selected')
+      .replay();
 });
 
-TEST_F('ChromeVoxEditingTest', 'TextAreaBrailleEmptyLine', function() {
+TEST_F('ChromeVoxEditingTest', 'TextAreaBrailleEmptyLine', async function() {
   const mockFeedback = this.createMockFeedback();
-  this.runWithLoadedTree(
-      '<textarea></textarea>', async function(root) {
-        const textarea = await this.focusFirstTextField(root);
-        textarea.setValue('test\n\none\ntwo\n\nthree');
-        await new Promise(
-            resolve =>
-                this.listenOnce(textarea, 'valueInTextFieldChanged', resolve));
-        mockFeedback.call(this.press(KeyCode.UP)).expectBraille('\n');
-        mockFeedback.call(this.press(KeyCode.UP)).expectBraille('two\n');
-        mockFeedback.call(this.press(KeyCode.UP)).expectBraille('one\n');
-        mockFeedback.call(this.press(KeyCode.UP)).expectBraille('\n');
-        mockFeedback.call(this.press(KeyCode.UP))
-            .expectBraille('test\nmled')
-            .replay();
-      });
+  const root = await this.runWithLoadedTree('<textarea></textarea>');
+  const textarea = await this.focusFirstTextField(root);
+  textarea.setValue('test\n\none\ntwo\n\nthree');
+  await new Promise(
+      resolve => this.listenOnce(textarea, 'valueInTextFieldChanged', resolve));
+  mockFeedback.call(this.press(KeyCode.UP)).expectBraille('\n');
+  mockFeedback.call(this.press(KeyCode.UP)).expectBraille('two\n');
+  mockFeedback.call(this.press(KeyCode.UP)).expectBraille('one\n');
+  mockFeedback.call(this.press(KeyCode.UP)).expectBraille('\n');
+  mockFeedback.call(this.press(KeyCode.UP))
+      .expectBraille('test\nmled')
+      .replay();
 });
 
-TEST_F('ChromeVoxEditingTest', 'MoveByCharacterIntent', function() {
+TEST_F('ChromeVoxEditingTest', 'MoveByCharacterIntent', async function() {
   const mockFeedback = this.createMockFeedback();
-  this.runWithLoadedTree(
-      `
+  const root = await this.runWithLoadedTree(`
     <div contenteditable role="textbox">
       <p>123</p>
       <p>456</p>
     </div>
-  `,
-      async function(root) {
-        await this.focusFirstTextField(root);
+  `);
+  await this.focusFirstTextField(root);
 
-        mockFeedback.call(this.press(KeyCode.RIGHT))
-            .expectSpeech('2')
-            .call(this.press(KeyCode.RIGHT))
-            .expectSpeech('3')
-            .call(this.press(KeyCode.RIGHT))
-            .expectSpeech('\n')
-            .call(this.press(KeyCode.RIGHT))
-            .expectSpeech('4')
-            .call(this.press(KeyCode.LEFT))
-            .expectSpeech('\n')
-            .call(this.press(KeyCode.LEFT))
-            .expectSpeech('3')
-            .replay();
-      });
+  mockFeedback.call(this.press(KeyCode.RIGHT))
+      .expectSpeech('2')
+      .call(this.press(KeyCode.RIGHT))
+      .expectSpeech('3')
+      .call(this.press(KeyCode.RIGHT))
+      .expectSpeech('\n')
+      .call(this.press(KeyCode.RIGHT))
+      .expectSpeech('4')
+      .call(this.press(KeyCode.LEFT))
+      .expectSpeech('\n')
+      .call(this.press(KeyCode.LEFT))
+      .expectSpeech('3')
+      .replay();
 });
 
-TEST_F('ChromeVoxEditingTest', 'MoveByLineIntent', function() {
+TEST_F('ChromeVoxEditingTest', 'MoveByLineIntent', async function() {
   const mockFeedback = this.createMockFeedback();
-  this.runWithLoadedTree(
-      `
+  const root = await this.runWithLoadedTree(`
     <div contenteditable role="textbox">
       <p>123</p>
       <p>456</p>
       <p>789</p>
     </div>
-  `,
-      async function(root) {
-        await this.focusFirstTextField(root);
+  `);
+  await this.focusFirstTextField(root);
 
-        mockFeedback.call(this.press(KeyCode.DOWN))
-            .expectSpeech('456')
-            .call(this.press(KeyCode.DOWN))
-            .expectSpeech('789')
-            .call(this.press(KeyCode.UP))
-            .expectSpeech('456')
-            .call(this.press(KeyCode.UP))
-            .expectSpeech('123')
-            .replay();
-      });
+  mockFeedback.call(this.press(KeyCode.DOWN))
+      .expectSpeech('456')
+      .call(this.press(KeyCode.DOWN))
+      .expectSpeech('789')
+      .call(this.press(KeyCode.UP))
+      .expectSpeech('456')
+      .call(this.press(KeyCode.UP))
+      .expectSpeech('123')
+      .replay();
 });
 
-TEST_F('ChromeVoxEditingTest', 'SelectAllBareTextContent', function() {
+TEST_F('ChromeVoxEditingTest', 'SelectAllBareTextContent', async function() {
   const mockFeedback = this.createMockFeedback();
-  this.runWithLoadedTree(
-      `
+  const root = await this.runWithLoadedTree(`
     <div contenteditable role="textbox">unread</div>
-  `,
-      async function(root) {
-        await this.focusFirstTextField(root);
+  `);
+  await this.focusFirstTextField(root);
 
-        mockFeedback.call(this.press(KeyCode.END, {ctrl: true}))
-            .expectSpeech('unread')
-            .call(this.press(KeyCode.A, {ctrl: true}))
-            .expectSpeech('unread', 'selected')
-            .replay();
-      });
+  mockFeedback.call(this.press(KeyCode.END, {ctrl: true}))
+      .expectSpeech('unread')
+      .call(this.press(KeyCode.A, {ctrl: true}))
+      .expectSpeech('unread', 'selected')
+      .replay();
 });
 
-TEST_F('ChromeVoxEditingTest', 'InputEvents', function() {
+TEST_F('ChromeVoxEditingTest', 'InputEvents', async function() {
   const site = `<input type="text"></input>`;
-  this.runWithLoadedTree(site, async function(root) {
-    const input = await this.focusFirstTextField(root);
+  const root = await this.runWithLoadedTree(site);
+  const input = await this.focusFirstTextField(root);
 
-    // EventType.TEXT_SELECTION_CHANGED fires on focus as well.
-    //
-    // TODO(nektar): Deprecate and remove TEXT_SELECTION_CHANGED.
-    event = await this.waitForEditableEvent();
-    assertEquals(EventType.TEXT_SELECTION_CHANGED, event.type);
-    assertEquals(input, event.target);
-    assertEquals('', input.value);
+  // EventType.TEXT_SELECTION_CHANGED fires on focus as well.
+  //
+  // TODO(nektar): Deprecate and remove TEXT_SELECTION_CHANGED.
+  event = await this.waitForEditableEvent();
+  assertEquals(EventType.TEXT_SELECTION_CHANGED, event.type);
+  assertEquals(input, event.target);
+  assertEquals('', input.value);
 
-    this.press(KeyCode.A)();
+  this.press(KeyCode.A)();
 
-    event = await this.waitForEditableEvent();
-    assertEquals(EventType.VALUE_IN_TEXT_FIELD_CHANGED, event.type);
-    assertEquals(input, event.target);
-    assertEquals('a', input.value);
+  event = await this.waitForEditableEvent();
+  assertEquals(EventType.VALUE_IN_TEXT_FIELD_CHANGED, event.type);
+  assertEquals(input, event.target);
+  assertEquals('a', input.value);
 
-    // We deliberately used EventType.TEXT_SELECTION_CHANGED instead of
-    // EventType.DOCUMENT_SELECTION_CHANGED for text fields.
-    event = await this.waitForEditableEvent();
-    assertEquals(EventType.TEXT_SELECTION_CHANGED, event.type);
-    assertEquals(input, event.target);
-    assertEquals('a', input.value);
+  // We deliberately used EventType.TEXT_SELECTION_CHANGED instead of
+  // EventType.DOCUMENT_SELECTION_CHANGED for text fields.
+  event = await this.waitForEditableEvent();
+  assertEquals(EventType.TEXT_SELECTION_CHANGED, event.type);
+  assertEquals(input, event.target);
+  assertEquals('a', input.value);
 
-    this.press(KeyCode.B)();
+  this.press(KeyCode.B)();
 
-    event = await this.waitForEditableEvent();
-    assertEquals(EventType.VALUE_IN_TEXT_FIELD_CHANGED, event.type);
-    assertEquals(input, event.target);
-    assertEquals('ab', input.value);
+  event = await this.waitForEditableEvent();
+  assertEquals(EventType.VALUE_IN_TEXT_FIELD_CHANGED, event.type);
+  assertEquals(input, event.target);
+  assertEquals('ab', input.value);
 
-    event = await this.waitForEditableEvent();
-    assertEquals(EventType.TEXT_SELECTION_CHANGED, event.type);
-    assertEquals(input, event.target);
-    assertEquals('ab', input.value);
-  });
+  event = await this.waitForEditableEvent();
+  assertEquals(EventType.TEXT_SELECTION_CHANGED, event.type);
+  assertEquals(input, event.target);
+  assertEquals('ab', input.value);
 });
 
-TEST_F('ChromeVoxEditingTest', 'TextAreaEvents', function() {
+TEST_F('ChromeVoxEditingTest', 'TextAreaEvents', async function() {
   const site = `<textarea></textarea>`;
-  this.runWithLoadedTree(site, async function(root) {
-    const textArea = await this.focusFirstTextField(root);
-    let event = await this.waitForEditableEvent();
-    assertEquals(EventType.DOCUMENT_SELECTION_CHANGED, event.type);
-    assertEquals(textArea, event.target);
-    assertEquals('', textArea.value);
+  const root = await this.runWithLoadedTree(site);
+  const textArea = await this.focusFirstTextField(root);
+  let event = await this.waitForEditableEvent();
+  assertEquals(EventType.DOCUMENT_SELECTION_CHANGED, event.type);
+  assertEquals(textArea, event.target);
+  assertEquals('', textArea.value);
 
-    this.press(KeyCode.A)();
+  this.press(KeyCode.A)();
 
-    event = await this.waitForEditableEvent();
-    assertEquals(EventType.DOCUMENT_SELECTION_CHANGED, event.type);
-    assertEquals(textArea, event.target);
-    assertEquals('a', textArea.value);
+  event = await this.waitForEditableEvent();
+  assertEquals(EventType.DOCUMENT_SELECTION_CHANGED, event.type);
+  assertEquals(textArea, event.target);
+  assertEquals('a', textArea.value);
 
-    this.press(KeyCode.B)();
+  this.press(KeyCode.B)();
 
-    event = await this.waitForEditableEvent();
-    assertEquals(EventType.DOCUMENT_SELECTION_CHANGED, event.type);
-    assertEquals(textArea, event.target);
-    assertEquals('ab', textArea.value);
-  });
+  event = await this.waitForEditableEvent();
+  assertEquals(EventType.DOCUMENT_SELECTION_CHANGED, event.type);
+  assertEquals(textArea, event.target);
+  assertEquals('ab', textArea.value);
 });
 
-TEST_F('ChromeVoxEditingTest', 'ContentEditableEvents', function() {
+TEST_F('ChromeVoxEditingTest', 'ContentEditableEvents', async function() {
   const site = `<div role="textbox" contenteditable></div>`;
-  this.runWithLoadedTree(site, async function(root) {
-    const contentEditable = await this.focusFirstTextField(root);
-    let event = await this.waitForEditableEvent();
-    assertEquals(EventType.DOCUMENT_SELECTION_CHANGED, event.type);
-    assertEquals(contentEditable, event.target);
-    assertEquals('', contentEditable.value);
+  const root = await this.runWithLoadedTree(site);
+  const contentEditable = await this.focusFirstTextField(root);
+  let event = await this.waitForEditableEvent();
+  assertEquals(EventType.DOCUMENT_SELECTION_CHANGED, event.type);
+  assertEquals(contentEditable, event.target);
+  assertEquals('', contentEditable.value);
 
-    this.press(KeyCode.A)();
+  this.press(KeyCode.A)();
 
-    event = await this.waitForEditableEvent();
-    assertEquals(EventType.DOCUMENT_SELECTION_CHANGED, event.type);
-    assertEquals(contentEditable, event.target);
-    assertEquals('a', contentEditable.value);
+  event = await this.waitForEditableEvent();
+  assertEquals(EventType.DOCUMENT_SELECTION_CHANGED, event.type);
+  assertEquals(contentEditable, event.target);
+  assertEquals('a', contentEditable.value);
 
-    this.press(KeyCode.B)();
+  this.press(KeyCode.B)();
 
-    event = await this.waitForEditableEvent();
-    assertEquals(EventType.DOCUMENT_SELECTION_CHANGED, event.type);
-    assertEquals(contentEditable, event.target);
-    assertEquals('ab', contentEditable.value);
-  });
+  event = await this.waitForEditableEvent();
+  assertEquals(EventType.DOCUMENT_SELECTION_CHANGED, event.type);
+  assertEquals(contentEditable, event.target);
+  assertEquals('ab', contentEditable.value);
 });
 
-TEST_F('ChromeVoxEditingTest', 'MarkedContent', function() {
+TEST_F('ChromeVoxEditingTest', 'MarkedContent', async function() {
   const mockFeedback = this.createMockFeedback();
   const site = `
     <div contenteditable role="textbox">
@@ -1644,27 +1557,26 @@
           role="deletion">everyone's</span></span><span> text.</span>
     </div>
   `;
-  this.runWithLoadedTree(site, async function(root) {
-    await this.focusFirstTextField(root);
+  const root = await this.runWithLoadedTree(site);
+  await this.focusFirstTextField(root);
 
-    mockFeedback.call(this.press(KeyCode.DOWN))
-        .expectSpeech('This is ')
-        .expectSpeech('Marked content', 'my', 'Marked content end')
-        .call(this.press(KeyCode.DOWN))
-        .expectSpeech('This is ')
-        .expectSpeech('Comment', 'your', 'Comment end')
-        .call(this.press(KeyCode.DOWN))
-        .expectSpeech('This is ')
-        .expectSpeech('Suggest', 'Insert', 'their', 'Insert end', 'Suggest end')
-        .call(this.press(KeyCode.DOWN))
-        .expectSpeech('This is ')
-        .expectSpeech(
-            'Suggest', 'Delete', `everyone's`, 'Delete end', 'Suggest end')
-        .replay();
-  });
+  mockFeedback.call(this.press(KeyCode.DOWN))
+      .expectSpeech('This is ')
+      .expectSpeech('Marked content', 'my', 'Marked content end')
+      .call(this.press(KeyCode.DOWN))
+      .expectSpeech('This is ')
+      .expectSpeech('Comment', 'your', 'Comment end')
+      .call(this.press(KeyCode.DOWN))
+      .expectSpeech('This is ')
+      .expectSpeech('Suggest', 'Insert', 'their', 'Insert end', 'Suggest end')
+      .call(this.press(KeyCode.DOWN))
+      .expectSpeech('This is ')
+      .expectSpeech(
+          'Suggest', 'Delete', `everyone's`, 'Delete end', 'Suggest end')
+      .replay();
 });
 
-TEST_F('ChromeVoxEditingTest', 'NestedInsertionDeletion', function() {
+TEST_F('ChromeVoxEditingTest', 'NestedInsertionDeletion', async function() {
   const mockFeedback = this.createMockFeedback();
   const site = `
     <div contenteditable role="textbox">
@@ -1676,20 +1588,19 @@
       <p>End</p>
     </div>
   `;
-  this.runWithLoadedTree(site, async function(root) {
-    await this.focusFirstTextField(root);
+  const root = await this.runWithLoadedTree(site);
+  await this.focusFirstTextField(root);
 
-    mockFeedback.call(this.press(KeyCode.DOWN))
-        .expectSpeech(
-            'I ', 'Suggest', 'Username', 'Insert', 'was', 'Insert end',
-            'Delete', 'am', 'Delete end', 'Suggest end', ' typing')
-        .call(this.press(KeyCode.DOWN))
-        .expectSpeech('End')
-        .replay();
-  });
+  mockFeedback.call(this.press(KeyCode.DOWN))
+      .expectSpeech(
+          'I ', 'Suggest', 'Username', 'Insert', 'was', 'Insert end', 'Delete',
+          'am', 'Delete end', 'Suggest end', ' typing')
+      .call(this.press(KeyCode.DOWN))
+      .expectSpeech('End')
+      .replay();
 });
 
-TEST_F('ChromeVoxEditingTest', 'MoveByCharSuggestions', function() {
+TEST_F('ChromeVoxEditingTest', 'MoveByCharSuggestions', async function() {
   const mockFeedback = this.createMockFeedback();
   const site = `
     <div contenteditable="true" role="textbox">
@@ -1701,44 +1612,43 @@
       <p>End</p>
     </div>
   `;
-  this.runWithLoadedTree(site, async function(root) {
-    await this.focusFirstTextField(root);
+  const root = await this.runWithLoadedTree(site);
+  await this.focusFirstTextField(root);
 
-    mockFeedback.call(this.press(KeyCode.DOWN))
-        .expectSpeech('I ')
-        // Move forward through line.
-        .call(this.press(KeyCode.RIGHT))
-        .expectSpeech(' ')
-        .call(this.press(KeyCode.RIGHT))
-        .expectSpeech('Suggest', 'Username', 'Insert', 'w')
-        .call(this.press(KeyCode.RIGHT))
-        .expectSpeech('a')
-        .call(this.press(KeyCode.RIGHT))
-        .expectSpeech('s')
-        .expectSpeech('Insert end')
-        .call(this.press(KeyCode.RIGHT))
-        .call(this.press(KeyCode.RIGHT))
-        .expectSpeech('Delete', 'a')
-        .call(this.press(KeyCode.RIGHT))
-        .expectSpeech('m')
-        .expectSpeech('Delete end', 'Suggest end')
-        // Move backward through the same line.
-        .call(this.press(KeyCode.LEFT))
-        .expectSpeech('Delete', 'a')
-        .call(this.press(KeyCode.LEFT))
-        .call(this.press(KeyCode.LEFT))
-        .expectSpeech('s', 'Insert end')
-        .call(this.press(KeyCode.LEFT))
-        .expectSpeech('a')
-        .call(this.press(KeyCode.LEFT))
-        .expectSpeech('Suggest', 'Insert', 'w')
-        .call(this.press(KeyCode.DOWN))
-        .expectSpeech('End')
-        .replay();
-  });
+  mockFeedback.call(this.press(KeyCode.DOWN))
+      .expectSpeech('I ')
+      // Move forward through line.
+      .call(this.press(KeyCode.RIGHT))
+      .expectSpeech(' ')
+      .call(this.press(KeyCode.RIGHT))
+      .expectSpeech('Suggest', 'Username', 'Insert', 'w')
+      .call(this.press(KeyCode.RIGHT))
+      .expectSpeech('a')
+      .call(this.press(KeyCode.RIGHT))
+      .expectSpeech('s')
+      .expectSpeech('Insert end')
+      .call(this.press(KeyCode.RIGHT))
+      .call(this.press(KeyCode.RIGHT))
+      .expectSpeech('Delete', 'a')
+      .call(this.press(KeyCode.RIGHT))
+      .expectSpeech('m')
+      .expectSpeech('Delete end', 'Suggest end')
+      // Move backward through the same line.
+      .call(this.press(KeyCode.LEFT))
+      .expectSpeech('Delete', 'a')
+      .call(this.press(KeyCode.LEFT))
+      .call(this.press(KeyCode.LEFT))
+      .expectSpeech('s', 'Insert end')
+      .call(this.press(KeyCode.LEFT))
+      .expectSpeech('a')
+      .call(this.press(KeyCode.LEFT))
+      .expectSpeech('Suggest', 'Insert', 'w')
+      .call(this.press(KeyCode.DOWN))
+      .expectSpeech('End')
+      .replay();
 });
 
-TEST_F('ChromeVoxEditingTest', 'MoveByWordSuggestions', function() {
+TEST_F('ChromeVoxEditingTest', 'MoveByWordSuggestions', async function() {
   const mockFeedback = this.createMockFeedback();
   const site = `
     <div contenteditable="true" role="textbox">
@@ -1750,34 +1660,34 @@
       <p>End</p>
     </div>
   `;
-  this.runWithLoadedTree(site, async function(root) {
-    await this.focusFirstTextField(root);
+  const root = await this.runWithLoadedTree(site);
+  await this.focusFirstTextField(root);
 
-    mockFeedback.call(this.press(KeyCode.DOWN))
-        .expectSpeech('I ')
-        // Move forward through line.
-        .call(this.press(KeyCode.RIGHT, {ctrl: true}))
-        .expectSpeech('I')
-        .call(this.press(KeyCode.RIGHT, {ctrl: true}))
-        .expectSpeech('Suggest', 'Username', 'Insert', 'was', 'Insert end')
-        .call(this.press(KeyCode.RIGHT, {ctrl: true}))
-        .expectSpeech('Delete', 'am', 'Delete end', 'Suggest end')
-        // Move backward through line.
-        .call(this.press(KeyCode.LEFT, {ctrl: true}))
-        .expectSpeech('Delete', 'am', 'Delete end', 'Suggest end')
-        .call(this.press(KeyCode.LEFT, {ctrl: true}))
-        .expectSpeech('Suggest', 'Username', 'Insert', 'was')
-        .call(this.press(KeyCode.LEFT, {ctrl: true}))
-        .expectSpeech('I')
-        .call(this.press(KeyCode.DOWN))
-        .expectSpeech('End')
-        .replay();
-  });
+  mockFeedback.call(this.press(KeyCode.DOWN))
+      .expectSpeech('I ')
+      // Move forward through line.
+      .call(this.press(KeyCode.RIGHT, {ctrl: true}))
+      .expectSpeech('I')
+      .call(this.press(KeyCode.RIGHT, {ctrl: true}))
+      .expectSpeech('Suggest', 'Username', 'Insert', 'was', 'Insert end')
+      .call(this.press(KeyCode.RIGHT, {ctrl: true}))
+      .expectSpeech('Delete', 'am', 'Delete end', 'Suggest end')
+      // Move backward through line.
+      .call(this.press(KeyCode.LEFT, {ctrl: true}))
+      .expectSpeech('Delete', 'am', 'Delete end', 'Suggest end')
+      .call(this.press(KeyCode.LEFT, {ctrl: true}))
+      .expectSpeech('Suggest', 'Username', 'Insert', 'was')
+      .call(this.press(KeyCode.LEFT, {ctrl: true}))
+      .expectSpeech('I')
+      .call(this.press(KeyCode.DOWN))
+      .expectSpeech('End')
+      .replay();
 });
 
-TEST_F('ChromeVoxEditingTest', 'MoveByWordSuggestionsNoIntents', function() {
-  const mockFeedback = this.createMockFeedback();
-  const site = `
+TEST_F(
+    'ChromeVoxEditingTest', 'MoveByWordSuggestionsNoIntents', async function() {
+      const mockFeedback = this.createMockFeedback();
+      const site = `
     <div contenteditable="true" role="textbox" id="textbox">
       <p>Start</p>
       <span>I </span>
@@ -1813,30 +1723,29 @@
       });
     </script>
   `;
-  this.runWithLoadedTree(site, async function(root) {
-    await this.focusFirstTextField(root);
+      const root = await this.runWithLoadedTree(site);
+      await this.focusFirstTextField(root);
 
-    mockFeedback.call(this.press(KeyCode.DOWN))
-        .expectSpeech('I ')
-        // Move forward through line.
+      mockFeedback.call(this.press(KeyCode.DOWN))
+          .expectSpeech('I ')
+          // Move forward through line.
 
-        // This first right arrow is allowed to be processed by the content
-        // editable.
-        .call(this.press(KeyCode.RIGHT, {ctrl: true}))
-        .expectSpeech('I')
+          // This first right arrow is allowed to be processed by the content
+          // editable.
+          .call(this.press(KeyCode.RIGHT, {ctrl: true}))
+          .expectSpeech('I')
 
-        // This next right is swallowed by the content editable mimicking custom
-        // rich editors. It manually moves selection (and looses intent data).
-        // We infer it by getting a command mapped for a raw control right arrow
-        // key.
-        .call(doCmd('nativeNextWord'))
-        .call(this.press(KeyCode.RIGHT, {ctrl: true}))
-        .expectSpeech('Suggest', 'Username', 'Insert', 'was', 'Insert end')
-        .replay();
-  });
-});
+          // This next right is swallowed by the content editable mimicking
+          // custom rich editors. It manually moves selection (and looses intent
+          // data). We infer it by getting a command mapped for a raw control
+          // right arrow key.
+          .call(doCmd('nativeNextWord'))
+          .call(this.press(KeyCode.RIGHT, {ctrl: true}))
+          .expectSpeech('Suggest', 'Username', 'Insert', 'was', 'Insert end')
+          .replay();
+    });
 
-TEST_F('ChromeVoxEditingTest', 'Separator', function() {
+TEST_F('ChromeVoxEditingTest', 'Separator', async function() {
   // In the past, an ARIA leaf role would cause subtree content to be removed.
   // However, the new decision is to not remove any content the user might
   // interact with.
@@ -1849,32 +1758,31 @@
       <p><span>World</span></p>
     </div>
   `;
-  this.runWithLoadedTree(site, async function(root) {
-    await this.focusFirstTextField(root);
+  const root = await this.runWithLoadedTree(site);
+  await this.focusFirstTextField(root);
 
-    mockFeedback.call(this.press(KeyCode.DOWN))
-        .expectSpeech('Hello')
-        .call(this.press(KeyCode.DOWN))
-        .expectSpeech('-', 'Separator')
-        .call(this.press(KeyCode.DOWN))
-        .expectSpeech('World')
+  mockFeedback.call(this.press(KeyCode.DOWN))
+      .expectSpeech('Hello')
+      .call(this.press(KeyCode.DOWN))
+      .expectSpeech('-', 'Separator')
+      .call(this.press(KeyCode.DOWN))
+      .expectSpeech('World')
 
-        .call(this.press(KeyCode.LEFT))
-        .expectNextSpeechUtteranceIsNot('\n')
-        // This reads the entire line (just one character).
-        .expectSpeech('-', 'Separator')
+      .call(this.press(KeyCode.LEFT))
+      .expectNextSpeechUtteranceIsNot('\n')
+      // This reads the entire line (just one character).
+      .expectSpeech('-', 'Separator')
 
-        .call(this.press(KeyCode.LEFT))
-        // This reads the single character.
-        .expectSpeech('-')
+      .call(this.press(KeyCode.LEFT))
+      // This reads the single character.
+      .expectSpeech('-')
 
-        .call(this.press(KeyCode.LEFT))
-        // Notice this reads the entire line which is generally undesirable
-        // except for special cases like this.
-        .expectSpeech('Hello')
+      .call(this.press(KeyCode.LEFT))
+      // Notice this reads the entire line which is generally undesirable
+      // except for special cases like this.
+      .expectSpeech('Hello')
 
-        .replay();
-  });
+      .replay();
 });
 
 // Test for the issue in crbug.com/1203840. This case was causing an infinite
@@ -1882,7 +1790,8 @@
 // workaround potential infinite loops correctly, and should be removed once the
 // proper fix is implemented in blink.
 TEST_F(
-    'ChromeVoxEditingTest', 'EditableLineInfiniteLoopWorkaround', function() {
+    'ChromeVoxEditingTest', 'EditableLineInfiniteLoopWorkaround',
+    async function() {
       const mockFeedback = this.createMockFeedback();
       const site = `
     <div contenteditable="true" role="textbox">
@@ -1900,171 +1809,165 @@
       <span>End</span>
     </div>
   `;
-      this.runWithLoadedTree(site, async function(root) {
-        await this.focusFirstTextField(root);
+      const root = await this.runWithLoadedTree(site);
+      await this.focusFirstTextField(root);
 
-        mockFeedback.call(this.press(KeyCode.DOWN))
-            .expectSpeech('This is a test')
-            .call(this.press(KeyCode.DOWN))
-            .expectSpeech('End')
-            .replay();
-      });
+      mockFeedback.call(this.press(KeyCode.DOWN))
+          .expectSpeech('This is a test')
+          .call(this.press(KeyCode.DOWN))
+          .expectSpeech('End')
+          .replay();
     });
 
 TEST_F(
     'ChromeVoxEditingTest', 'TextEditHandlerCreatesAutomationEditable',
-    function() {
+    async function() {
       const site = `
     <input type="text"></input>
   `;
-      this.runWithLoadedTree(site, async function(root) {
-        const input = await this.focusFirstTextField(root);
+      const root = await this.runWithLoadedTree(site);
+      const input = await this.focusFirstTextField(root);
 
-        // The initial real input is a simple non-rich text field.
-        assertEquals(
-            'AutomationEditableText',
-            DesktopAutomationInterface.instance.textEditHandler.editableText_
-                .constructor.name,
-            'Real text field was not a non-rich text.');
+      // The initial real input is a simple non-rich text field.
+      assertEquals(
+          'AutomationEditableText',
+          DesktopAutomationInterface.instance.textEditHandler.editableText_
+              .constructor.name,
+          'Real text field was not a non-rich text.');
 
-        // Now, we will override some properties directly to
-        // ensure we don't depend on Blink's behaviors which can change based
-        // on style. We want to work directly with only the automation api
-        // itself to ensure we have full coverage.
-        let htmlAttributes = {};
-        let htmlTag = '';
-        let state = {};
-        Object.defineProperty(
-            input, 'htmlAttributes', {get: () => htmlAttributes});
-        Object.defineProperty(input, 'htmlTag', {get: () => htmlTag});
-        Object.defineProperty(input, 'state', {get: () => state});
+      // Now, we will override some properties directly to
+      // ensure we don't depend on Blink's behaviors which can change based
+      // on style. We want to work directly with only the automation api
+      // itself to ensure we have full coverage.
+      let htmlAttributes = {};
+      let htmlTag = '';
+      let state = {};
+      Object.defineProperty(
+          input, 'htmlAttributes', {get: () => htmlAttributes});
+      Object.defineProperty(input, 'htmlTag', {get: () => htmlTag});
+      Object.defineProperty(input, 'state', {get: () => state});
 
-        // An invalid editable.
-        let didThrow = false;
-        let handler;
-        try {
-          handler = new TextEditHandler(input);
-        } catch (e) {
-          didThrow = true;
-        }
-        assertTrue(didThrow, 'Non-editable created editable handler.');
-
-        // A simple editable.
-        htmlAttributes = {};
-        htmlTag = '';
-        state = {editable: true};
+      // An invalid editable.
+      let didThrow = false;
+      let handler;
+      try {
         handler = new TextEditHandler(input);
-        assertEquals(
-            'AutomationEditableText', handler.editableText_.constructor.name,
-            'Incorrect backing object for simple editable.');
+      } catch (e) {
+        didThrow = true;
+      }
+      assertTrue(didThrow, 'Non-editable created editable handler.');
 
-        // A non-rich editable via multiline.
-        htmlAttributes = {};
-        htmlTag = '';
-        state = {editable: true, multiline: true};
-        handler = new TextEditHandler(input);
-        assertEquals(
-            'AutomationEditableText', handler.editableText_.constructor.name,
-            'Incorrect object for multiline editable.');
+      // A simple editable.
+      htmlAttributes = {};
+      htmlTag = '';
+      state = {editable: true};
+      handler = new TextEditHandler(input);
+      assertEquals(
+          'AutomationEditableText', handler.editableText_.constructor.name,
+          'Incorrect backing object for simple editable.');
 
-        // A rich editable via textarea tag.
-        htmlAttributes = {};
-        htmlTag = 'textarea';
-        state = {editable: true};
-        handler = new TextEditHandler(input);
-        assertEquals(
-            'AutomationRichEditableText',
-            handler.editableText_.constructor.name,
-            'Incorrect object for textarea html tag.');
+      // A non-rich editable via multiline.
+      htmlAttributes = {};
+      htmlTag = '';
+      state = {editable: true, multiline: true};
+      handler = new TextEditHandler(input);
+      assertEquals(
+          'AutomationEditableText', handler.editableText_.constructor.name,
+          'Incorrect object for multiline editable.');
 
-        // A rich editable via state.
-        htmlAttributes = {};
-        htmlTag = '';
-        state = {editable: true, richlyEditable: true};
-        handler = new TextEditHandler(input);
-        assertEquals(
-            'AutomationRichEditableText',
-            handler.editableText_.constructor.name,
-            'Incorrect object for richly editable state.');
+      // A rich editable via textarea tag.
+      htmlAttributes = {};
+      htmlTag = 'textarea';
+      state = {editable: true};
+      handler = new TextEditHandler(input);
+      assertEquals(
+          'AutomationRichEditableText', handler.editableText_.constructor.name,
+          'Incorrect object for textarea html tag.');
 
-        // A rich editable via contenteditable. (aka <div contenteditable>).
-        htmlAttributes = {contenteditable: ''};
-        htmlTag = '';
-        state = {editable: true};
-        handler = new TextEditHandler(input);
-        assertEquals(
-            'AutomationRichEditableText',
-            handler.editableText_.constructor.name,
-            'Incorrect object for content editable.');
+      // A rich editable via state.
+      htmlAttributes = {};
+      htmlTag = '';
+      state = {editable: true, richlyEditable: true};
+      handler = new TextEditHandler(input);
+      assertEquals(
+          'AutomationRichEditableText', handler.editableText_.constructor.name,
+          'Incorrect object for richly editable state.');
 
-        // A rich editable via contenteditable. (aka <div
-        // contenteditable=true>).
-        htmlAttributes = {contenteditable: 'true'};
-        htmlTag = '';
-        state = {editable: true};
-        handler = new TextEditHandler(input);
-        assertEquals(
-            'AutomationRichEditableText',
-            handler.editableText_.constructor.name,
-            'Incorrect object for content editable true.');
+      // A rich editable via contenteditable. (aka <div contenteditable>).
+      htmlAttributes = {contenteditable: ''};
+      htmlTag = '';
+      state = {editable: true};
+      handler = new TextEditHandler(input);
+      assertEquals(
+          'AutomationRichEditableText', handler.editableText_.constructor.name,
+          'Incorrect object for content editable.');
 
-        // Note that it is not possible to have <div
-        // contenteditable="someInvalidValue"> or <div contenteditable=false>
-        // and still have the div expose editable state, so we never check
-        // that.
-      });
+      // A rich editable via contenteditable. (aka <div
+      // contenteditable=true>).
+      htmlAttributes = {contenteditable: 'true'};
+      htmlTag = '';
+      state = {editable: true};
+      handler = new TextEditHandler(input);
+      assertEquals(
+          'AutomationRichEditableText', handler.editableText_.constructor.name,
+          'Incorrect object for content editable true.');
+
+      // Note that it is not possible to have <div
+      // contenteditable="someInvalidValue"> or <div contenteditable=false>
+      // and still have the div expose editable state, so we never check
+      // that.
     });
 
 // TODO(https://crbug.com/1254742): flakes due to underlying bug with
 // accessibility intents.
-TEST_F('ChromeVoxEditingTest', 'DISABLED_ParagraphNavigation', function() {
-  const mockFeedback = this.createMockFeedback();
-  const site = `
+TEST_F(
+    'ChromeVoxEditingTest', 'DISABLED_ParagraphNavigation', async function() {
+      const mockFeedback = this.createMockFeedback();
+      const site = `
     <div contenteditable role="textbox"
         style='max-width: 5px; overflow-wrap: normal'>
       <p>This is paragraph number one.</p>
       <p>Another paragraph, number two.</p>
     </div>
   `;
-  this.runWithLoadedTree(site, async function(root) {
-    await this.focusFirstTextField(root);
+      const root = await this.runWithLoadedTree(site);
+      await this.focusFirstTextField(root);
 
-    // We bind specific callbacks to send keys here because EventGenerator
-    // (which sends key down and up) does not seem to work with these
-    // shortcuts.
-    const ctrlDown = () => chrome.accessibilityPrivate.sendSyntheticKeyEvent({
-      type: chrome.accessibilityPrivate.SyntheticKeyboardEventType.KEYDOWN,
-      keyCode: KeyCode.DOWN,
-      modifiers: {ctrl: true}
-    });
-    const ctrlUp = () => chrome.accessibilityPrivate.sendSyntheticKeyEvent({
-      type: chrome.accessibilityPrivate.SyntheticKeyboardEventType.KEYDOWN,
-      keyCode: KeyCode.UP,
-      modifiers: {ctrl: true}
-    });
+      // We bind specific callbacks to send keys here because EventGenerator
+      // (which sends key down and up) does not seem to work with these
+      // shortcuts.
+      const ctrlDown = () => chrome.accessibilityPrivate.sendSyntheticKeyEvent({
+        type: chrome.accessibilityPrivate.SyntheticKeyboardEventType.KEYDOWN,
+        keyCode: KeyCode.DOWN,
+        modifiers: {ctrl: true}
+      });
+      const ctrlUp = () => chrome.accessibilityPrivate.sendSyntheticKeyEvent({
+        type: chrome.accessibilityPrivate.SyntheticKeyboardEventType.KEYDOWN,
+        keyCode: KeyCode.UP,
+        modifiers: {ctrl: true}
+      });
 
-    mockFeedback.expectSpeech('Text area')
-        .call(ctrlDown)
-        .expectSpeech('Another paragraph, number two.')
-        .call(this.press(KeyCode.DOWN))
-        .expectSpeech('paragraph, ')
-        .call(ctrlUp)
-        .expectSpeech('This is paragraph number one.')
-        .call(this.press(KeyCode.UP))
-        .expectSpeech('number ')
-        .call(this.press(KeyCode.UP))
-        .expectSpeech('paragraph ')
-        .call(ctrlDown)
-        .expectSpeech('Another paragraph, number two.')
-        .call(this.press(KeyCode.DOWN))
-        .expectSpeech('paragraph, ')
-        .replay();
-  });
-});
+      mockFeedback.expectSpeech('Text area')
+          .call(ctrlDown)
+          .expectSpeech('Another paragraph, number two.')
+          .call(this.press(KeyCode.DOWN))
+          .expectSpeech('paragraph, ')
+          .call(ctrlUp)
+          .expectSpeech('This is paragraph number one.')
+          .call(this.press(KeyCode.UP))
+          .expectSpeech('number ')
+          .call(this.press(KeyCode.UP))
+          .expectSpeech('paragraph ')
+          .call(ctrlDown)
+          .expectSpeech('Another paragraph, number two.')
+          .call(this.press(KeyCode.DOWN))
+          .expectSpeech('paragraph, ')
+          .replay();
+    });
 
 TEST_F(
     'ChromeVoxEditingTest', 'StartAndEndOfOutputStopAtEditableRoot',
-    function() {
+    async function() {
       const mockFeedback = this.createMockFeedback();
       const site = `
     <div role="article">
@@ -2073,20 +1976,19 @@
       </div>
     </div>
   `;
-      this.runWithLoadedTree(site, async function(root) {
-        await this.focusFirstTextField(root);
-        mockFeedback.expectSpeech('Text area')
-            .call(this.press(KeyCode.DOWN))
-            .expectSpeech('world')
-            .call(this.press(KeyCode.UP))
-            .expectNextSpeechUtteranceIsNot('Article')
-            .expectNextSpeechUtteranceIsNot('Article end')
-            .expectSpeech('hello')
-            .replay();
-      });
+      const root = await this.runWithLoadedTree(site);
+      await this.focusFirstTextField(root);
+      mockFeedback.expectSpeech('Text area')
+          .call(this.press(KeyCode.DOWN))
+          .expectSpeech('world')
+          .call(this.press(KeyCode.UP))
+          .expectNextSpeechUtteranceIsNot('Article')
+          .expectNextSpeechUtteranceIsNot('Article end')
+          .expectSpeech('hello')
+          .replay();
     });
 
-TEST_F('ChromeVoxEditingTest', 'TableNavigation', function() {
+TEST_F('ChromeVoxEditingTest', 'TableNavigation', async function() {
   const mockFeedback = this.createMockFeedback();
   const site = `
     <div contenteditable role="textbox" tabindex=0>
@@ -2096,161 +1998,174 @@
       </table>
     </div>
   `;
-  this.runWithLoadedTree(site, async function(root) {
-    await this.focusFirstTextField(root);
+  const root = await this.runWithLoadedTree(site);
+  await this.focusFirstTextField(root);
 
-    mockFeedback.expectSpeech('Text area')
-        .call(this.press(KeyCode.RIGHT))
-        .call(this.press(KeyCode.RIGHT))
-        .expectSpeech('e')
-        .call(doCmd('nextCol'))
-        .expectSpeech('goodbye')
-        .expectSpeech('row 1 column 2')
-        .call(this.press(KeyCode.RIGHT))
-        .expectSpeech('o')
-        .call(doCmd('previousCol'))
-        .expectSpeech('hello', 'world')
-        .expectSpeech('row 1 column 1')
-        .call(this.press(KeyCode.RIGHT))
-        .expectSpeech('e')
-        .replay();
-  });
+  mockFeedback.expectSpeech('Text area')
+      .call(this.press(KeyCode.RIGHT))
+      .call(this.press(KeyCode.RIGHT))
+      .expectSpeech('e')
+      .call(doCmd('nextCol'))
+      .expectSpeech('goodbye')
+      .expectSpeech('row 1 column 2')
+      .call(this.press(KeyCode.RIGHT))
+      .expectSpeech('o')
+      .call(doCmd('previousCol'))
+      .expectSpeech('hello', 'world')
+      .expectSpeech('row 1 column 1')
+      .call(this.press(KeyCode.RIGHT))
+      .expectSpeech('e')
+      .replay();
 });
 
 TEST_F(
-    'ChromeVoxEditingTest', 'InputTextBrailleContractions', function() {
+    'ChromeVoxEditingTest', 'InputTextBrailleContractions', async function() {
       const site = `
     <input type=text value="about that"></input>
   `;
-      this.runWithLoadedTree(site, async function(root) {
-        await this.focusFirstTextField(root);
+      const root = await this.runWithLoadedTree(site);
+      await this.focusFirstTextField(root);
 
-        // In case LibLouis takes a while to load.
-        if (!ChromeVox.braille.displayManager_.translatorManager_.liblouis_
-                 .isLoaded()) {
-          await new Promise(r => {
-            ChromeVox.braille.displayManager_.translatorManager_.liblouis_
-                .onInstanceLoad_ = r;
-          });
-        }
+      // In case LibLouis takes a while to load.
+      if (!ChromeVox.braille.displayManager_.translatorManager_.liblouis_
+               .isLoaded()) {
+        await new Promise(r => {
+          ChromeVox.braille.displayManager_.translatorManager_.liblouis_
+              .onInstanceLoad_ = r;
+        });
+      }
 
-        // Fake an available display.
-        ChromeVox.braille.displayManager_.refreshDisplayState_(
-            {available: true, textRowCount: 1, textColumnCount: 40});
+      // Fake an available display.
+      ChromeVox.braille.displayManager_.refreshDisplayState_(
+          {available: true, textRowCount: 1, textColumnCount: 40});
 
-        // Set braille to use 6-dot braille (which is defaulted to UEB grade 2
-        // contracted braille).
-        localStorage['brailleTable'] = 'en-ueb-g2';
+      // Set braille to use 6-dot braille (which is defaulted to UEB grade 2
+      // contracted braille).
+      localStorage['brailleTable'] = 'en-ueb-g2';
 
-        // Wait for it to be fully refreshed (liblouis loads the new tables, our
-        // translators are re-created).
-        await BrailleBackground.getInstance()
+      // Wait for it to be fully refreshed (liblouis loads the new tables, our
+      // translators are re-created).
+      await BrailleBackground.getInstance()
+          .getTranslatorManager()
+          .loadTablesForTest();
+
+      // Fake an available display.
+      ChromeVox.braille.displayManager_.refreshDisplayState_(
+          {available: true, textRowCount: 1, textColumnCount: 40});
+
+      // Set braille to use 6-dot braille (which is defaulted to UEB grade 2
+      // contracted braille).
+      localStorage['brailleTable'] = 'en-ueb-g2';
+      BrailleBackground.getInstance().getTranslatorManager().refresh(
+          localStorage['brailleTable']);
+      // Wait for it to be fully refreshed (liblouis loads the new tables, our
+      // translators are re-created).
+      await new Promise(r => {
+        BrailleBackground.getInstance()
             .getTranslatorManager()
-            .loadTablesForTest();
-
-        async function waitForBrailleDots(expectedDots) {
-          return new Promise(r => {
-            chrome.brailleDisplayPrivate.writeDots = (dotsBuffer) => {
-              const view = new Uint8Array(dotsBuffer);
-              const dots = new Array(view.length);
-              view.forEach((item, index) => dots[index] = item.toString(2));
-              if (expectedDots.toString() === dots.toString()) {
-                r();
-              }
-            };
-          });
-        }
-
-        this.press(KeyCode.END)();
-
-        // This test intentionally leaves the raw binary encoding for braille.
-        // Dots are read from right to left.
-        await waitForBrailleDots([
-          // 'ab' is 'about' in UEB Grade 2.
-          1 /* a */, 11 /* b */,
-
-          0 /* space */,
-
-          11110 /* t */, 10011 /* h */, 1 /* a */, 11110 /* t */,
-
-          11000000 /* cursor _ */,
-
-          101011 /* ed contraction */
-        ]);
-
-        this.press(KeyCode.HOME)();
-        await waitForBrailleDots([
-          11000001 /* a with a cursor _*/, 11 /* b */, 10101 /* o */,
-          100101 /* u */, 11110 /* t */,
-
-          0 /* space */,
-
-          // 't' by itself is contracted as 'that'.
-          11110 /* t */,
-
-          0 /* space */,
-
-          101011 /* ed contraction */
-        ]);
+            .addChangeListener(r);
       });
+
+      async function waitForBrailleDots(expectedDots) {
+        return new Promise(r => {
+          chrome.brailleDisplayPrivate.writeDots = (dotsBuffer) => {
+            const view = new Uint8Array(dotsBuffer);
+            const dots = new Array(view.length);
+            view.forEach((item, index) => dots[index] = item.toString(2));
+            if (expectedDots.toString() === dots.toString()) {
+              r();
+            }
+          };
+        });
+      }
+
+      this.press(KeyCode.END)();
+
+      // This test intentionally leaves the raw binary encoding for braille.
+      // Dots are read from right to left.
+      await waitForBrailleDots([
+        // 'ab' is 'about' in UEB Grade 2.
+        1 /* a */, 11 /* b */,
+
+        0 /* space */,
+
+        11110 /* t */, 10011 /* h */, 1 /* a */, 11110 /* t */,
+
+        11000000 /* cursor _ */,
+
+        101011 /* ed contraction */
+      ]);
+
+      this.press(KeyCode.HOME)();
+      await waitForBrailleDots([
+        11000001 /* a with a cursor _*/, 11 /* b */, 10101 /* o */,
+        100101 /* u */, 11110 /* t */,
+
+        0 /* space */,
+
+        // 't' by itself is contracted as 'that'.
+        11110 /* t */,
+
+        0 /* space */,
+
+        101011 /* ed contraction */
+      ]);
     });
 
 
-TEST_F('ChromeVoxEditingTest', 'ContextMenus', function() {
+TEST_F('ChromeVoxEditingTest', 'ContextMenus', async function() {
   const mockFeedback = this.createMockFeedback();
   const site = `
     <textarea>abc</textarea>
   `;
-  this.runWithLoadedTree(site, async function(root) {
-    await this.focusFirstTextField(root);
+  const root = await this.runWithLoadedTree(site);
+  await this.focusFirstTextField(root);
 
-    const textField = root.find({role: RoleType.TEXT_FIELD});
-    mockFeedback.expectSpeech('Text area')
-        .call(() => {
-          textField.setSelection(0, 2);
-        })
-        .expectSpeech('ab', 'selected')
-        .call(doCmd('contextMenu'))
-        .expectSpeech(' menu opened')
-        .call(this.press(KeyCode.ESCAPE))
-        .expectSpeech('ab', 'selected')
-        .replay();
-  });
+  const textField = root.find({role: RoleType.TEXT_FIELD});
+  mockFeedback.expectSpeech('Text area')
+      .call(() => {
+        textField.setSelection(0, 2);
+      })
+      .expectSpeech('ab', 'selected')
+      .call(doCmd('contextMenu'))
+      .expectSpeech(' menu opened')
+      .call(this.press(KeyCode.ESCAPE))
+      .expectSpeech('ab', 'selected')
+      .replay();
 });
 
-TEST_F('ChromeVoxEditingTest', 'NativeCharWordCommands', function() {
+TEST_F('ChromeVoxEditingTest', 'NativeCharWordCommands', async function() {
   const mockFeedback = this.createMockFeedback();
   const site = `
     <p>start</p>
     <div role="textbox" contenteditable>This is a test</div>
   `;
-  this.runWithLoadedTree(site, async function(root) {
-    await this.focusFirstTextField(root);
+  const root = await this.runWithLoadedTree(site);
+  await this.focusFirstTextField(root);
 
-    const textField = root.find({role: RoleType.TEXT_FIELD});
-    mockFeedback.expectSpeech('Text area')
-        .call(this.press(KeyCode.HOME, {ctrl: true}))
-        .call(this.press(KeyCode.RIGHT))
-        .expectSpeech('h')
-        .call(this.press(KeyCode.RIGHT))
-        .expectSpeech('i')
-        .call(this.press(KeyCode.LEFT))
-        .expectSpeech('h')
+  const textField = root.find({role: RoleType.TEXT_FIELD});
+  mockFeedback.expectSpeech('Text area')
+      .call(this.press(KeyCode.HOME, {ctrl: true}))
+      .call(this.press(KeyCode.RIGHT))
+      .expectSpeech('h')
+      .call(this.press(KeyCode.RIGHT))
+      .expectSpeech('i')
+      .call(this.press(KeyCode.LEFT))
+      .expectSpeech('h')
 
-        .call(this.press(KeyCode.RIGHT, {ctrl: true}))
-        .expectSpeech('This')
-        .call(this.press(KeyCode.RIGHT, {ctrl: true}))
-        .expectSpeech('is')
-        .call(this.press(KeyCode.LEFT, {ctrl: true}))
-        .expectSpeech('is')
-        .call(this.press(KeyCode.LEFT, {ctrl: true}))
-        .expectSpeech('This')
+      .call(this.press(KeyCode.RIGHT, {ctrl: true}))
+      .expectSpeech('This')
+      .call(this.press(KeyCode.RIGHT, {ctrl: true}))
+      .expectSpeech('is')
+      .call(this.press(KeyCode.LEFT, {ctrl: true}))
+      .expectSpeech('is')
+      .call(this.press(KeyCode.LEFT, {ctrl: true}))
+      .expectSpeech('This')
 
-        .replay();
-  });
+      .replay();
 });
 
-TEST_F('ChromeVoxEditingTest', 'TablesWithEmptyCells', function() {
+TEST_F('ChromeVoxEditingTest', 'TablesWithEmptyCells', async function() {
   const mockFeedback = this.createMockFeedback();
   const site = `
     <div contenteditable="true" role="textbox">
@@ -2270,40 +2185,40 @@
       </table>
     </div><div></div></div>
   `;
-  this.runWithLoadedTree(site, async function(root) {
-    await this.focusFirstTextField(root);
+  const root = await this.runWithLoadedTree(site);
+  await this.focusFirstTextField(root);
 
-    const textField = root.find({role: RoleType.TEXT_FIELD});
-    mockFeedback.expectSpeech('Text area')
-        .call(this.press(KeyCode.HOME, {ctrl: true}))
-        .call(this.press(KeyCode.RIGHT))
-        .call(this.press(KeyCode.RIGHT))
-        .call(this.press(KeyCode.RIGHT))
-        // This first cell is on a new line.
-        .expectSpeech('\n', 'row 1 column 1')
-        .call(this.press(KeyCode.RIGHT))
-        // Non-breaking spaces (\u00a0) get preprocessed later by TtsBackground
-        // to ' '. This comes as part of speak line output in
-        // AutomationRichEditableText.
-        .expectSpeech('\u00a0')
-        .call(this.press(KeyCode.RIGHT))
-        .expectSpeech('\u00a0', 'row 1 column 2')
-        .call(this.press(KeyCode.RIGHT))
-        .expectSpeech('\u00a0')
-        .call(this.press(KeyCode.RIGHT))
-        .expectSpeech('\u00a0', 'row 2 column 1')
-        .call(this.press(KeyCode.RIGHT))
-        .expectSpeech('\u00a0')
-        .call(this.press(KeyCode.RIGHT))
-        .expectSpeech('\u00a0', 'row 2 column 2')
+  const textField = root.find({role: RoleType.TEXT_FIELD});
+  mockFeedback.expectSpeech('Text area')
+      .call(this.press(KeyCode.HOME, {ctrl: true}))
+      .call(this.press(KeyCode.RIGHT))
+      .call(this.press(KeyCode.RIGHT))
+      .call(this.press(KeyCode.RIGHT))
+      // This first cell is on a new line.
+      .expectSpeech('\n', 'row 1 column 1')
+      .call(this.press(KeyCode.RIGHT))
+      // Non-breaking spaces (\u00a0) get preprocessed later by TtsBackground
+      // to ' '. This comes as part of speak line output in
+      // AutomationRichEditableText.
+      .expectSpeech('\u00a0')
+      .call(this.press(KeyCode.RIGHT))
+      .expectSpeech('\u00a0', 'row 1 column 2')
+      .call(this.press(KeyCode.RIGHT))
+      .expectSpeech('\u00a0')
+      .call(this.press(KeyCode.RIGHT))
+      .expectSpeech('\u00a0', 'row 2 column 1')
+      .call(this.press(KeyCode.RIGHT))
+      .expectSpeech('\u00a0')
+      .call(this.press(KeyCode.RIGHT))
+      .expectSpeech('\u00a0', 'row 2 column 2')
 
-        .replay();
-  });
+      .replay();
 });
 
-TEST_F('ChromeVoxEditingTest', 'NonbreakingSpaceNewLineOrSpace', function() {
-  const mockFeedback = this.createMockFeedback();
-  const site = `
+TEST_F(
+    'ChromeVoxEditingTest', 'NonbreakingSpaceNewLineOrSpace', async function() {
+      const mockFeedback = this.createMockFeedback();
+      const site = `
     <div contenteditable="true" role="textbox">
       <p>first line</p>
       <div><span>&nbsp;</span></div>
@@ -2312,68 +2227,67 @@
       <p>last line</p>
     </div>
   `;
-  this.runWithLoadedTree(site, async function(root) {
-    await this.focusFirstTextField(root);
+      const root = await this.runWithLoadedTree(site);
+      await this.focusFirstTextField(root);
 
-    const textField = root.find({role: RoleType.TEXT_FIELD});
-    mockFeedback.expectSpeech('Text area')
-        .call(this.press(KeyCode.HOME, {ctrl: true}))
-        .call(this.press(KeyCode.DOWN))
-        .expectSpeech('\n')
-        .call(this.press(KeyCode.DOWN))
-        .expectSpeech('\n')
-        .call(this.press(KeyCode.DOWN))
-        .expectSpeech('\n')
-        .call(this.press(KeyCode.DOWN))
-        .expectSpeech('last line')
+      const textField = root.find({role: RoleType.TEXT_FIELD});
+      mockFeedback.expectSpeech('Text area')
+          .call(this.press(KeyCode.HOME, {ctrl: true}))
+          .call(this.press(KeyCode.DOWN))
+          .expectSpeech('\n')
+          .call(this.press(KeyCode.DOWN))
+          .expectSpeech('\n')
+          .call(this.press(KeyCode.DOWN))
+          .expectSpeech('\n')
+          .call(this.press(KeyCode.DOWN))
+          .expectSpeech('last line')
 
-        .call(this.press(KeyCode.UP))
-        .expectSpeech('\n')
-        .call(this.press(KeyCode.UP))
-        .expectSpeech('\n')
-        .call(this.press(KeyCode.UP))
-        .expectSpeech('\n')
-        .call(this.press(KeyCode.UP))
-        .expectSpeech('first line')
+          .call(this.press(KeyCode.UP))
+          .expectSpeech('\n')
+          .call(this.press(KeyCode.UP))
+          .expectSpeech('\n')
+          .call(this.press(KeyCode.UP))
+          .expectSpeech('\n')
+          .call(this.press(KeyCode.UP))
+          .expectSpeech('first line')
 
-        .call(this.press(KeyCode.DOWN))
-        .expectSpeech('\n')
+          .call(this.press(KeyCode.DOWN))
+          .expectSpeech('\n')
 
-        .call(this.press(KeyCode.RIGHT))
-        .expectSpeech('\u00a0')
-        .call(this.press(KeyCode.RIGHT))
-        .expectSpeech('\u00a0')
-        .call(this.press(KeyCode.RIGHT))
-        .expectSpeech('\u00a0')
-        .call(this.press(KeyCode.RIGHT))
-        .expectSpeech('\u00a0')
-        .call(this.press(KeyCode.RIGHT))
-        .expectSpeech('\u00a0')
-        .call(this.press(KeyCode.RIGHT))
-        .expectSpeech('l')
+          .call(this.press(KeyCode.RIGHT))
+          .expectSpeech('\u00a0')
+          .call(this.press(KeyCode.RIGHT))
+          .expectSpeech('\u00a0')
+          .call(this.press(KeyCode.RIGHT))
+          .expectSpeech('\u00a0')
+          .call(this.press(KeyCode.RIGHT))
+          .expectSpeech('\u00a0')
+          .call(this.press(KeyCode.RIGHT))
+          .expectSpeech('\u00a0')
+          .call(this.press(KeyCode.RIGHT))
+          .expectSpeech('l')
 
-        .call(this.press(KeyCode.LEFT))
-        .expectSpeech('\u00a0')
-        .call(this.press(KeyCode.LEFT))
-        .expectSpeech('\u00a0')
-        .call(this.press(KeyCode.LEFT))
-        .expectSpeech('\u00a0')
-        .call(this.press(KeyCode.LEFT))
-        .expectSpeech('\u00a0')
-        .call(this.press(KeyCode.LEFT))
-        .expectSpeech('\u00a0')
-        .call(this.press(KeyCode.LEFT))
-        .expectSpeech('\u00a0')
-        .call(this.press(KeyCode.LEFT))
-        .expectSpeech('\n')
-        .call(this.press(KeyCode.LEFT))
-        .expectSpeech('e')
+          .call(this.press(KeyCode.LEFT))
+          .expectSpeech('\u00a0')
+          .call(this.press(KeyCode.LEFT))
+          .expectSpeech('\u00a0')
+          .call(this.press(KeyCode.LEFT))
+          .expectSpeech('\u00a0')
+          .call(this.press(KeyCode.LEFT))
+          .expectSpeech('\u00a0')
+          .call(this.press(KeyCode.LEFT))
+          .expectSpeech('\u00a0')
+          .call(this.press(KeyCode.LEFT))
+          .expectSpeech('\u00a0')
+          .call(this.press(KeyCode.LEFT))
+          .expectSpeech('\n')
+          .call(this.press(KeyCode.LEFT))
+          .expectSpeech('e')
 
-        .replay();
-  });
-});
+          .replay();
+    });
 
-TEST_F('ChromeVoxEditingTest', 'JumpCommandsSyncSelection', function() {
+TEST_F('ChromeVoxEditingTest', 'JumpCommandsSyncSelection', async function() {
   const mockFeedback = this.createMockFeedback();
   const site = `
     <div contenteditable="true" role="textbox">
@@ -2383,28 +2297,27 @@
       <table border=1><r><td>fifth</td></tr></table>
     </div>
   `;
-  this.runWithLoadedTree(site, async function(root) {
-    await this.focusFirstTextField(root);
+  const root = await this.runWithLoadedTree(site);
+  await this.focusFirstTextField(root);
 
-    const textField = root.find({role: RoleType.TEXT_FIELD});
-    mockFeedback.expectSpeech('Text area')
-        .call(doCmd('nextTable'))
-        .expectSpeech('fifth', 'row 1 column 1', 'Table , 1 by 1')
+  const textField = root.find({role: RoleType.TEXT_FIELD});
+  mockFeedback.expectSpeech('Text area')
+      .call(doCmd('nextTable'))
+      .expectSpeech('fifth', 'row 1 column 1', 'Table , 1 by 1')
 
-        // Verifies selection is where we expect.
-        .call(this.press(KeyCode.RIGHT, {shift: true, ctrl: true}))
-        .expectSpeech('fifth', 'row 1 column 1', 'Table , 1 by 1', 'selected')
+      // Verifies selection is where we expect.
+      .call(this.press(KeyCode.RIGHT, {shift: true, ctrl: true}))
+      .expectSpeech('fifth', 'row 1 column 1', 'Table , 1 by 1', 'selected')
 
-        .call(doCmd('previousHeading'))
-        .expectSpeech('second', 'Heading 1')
-        .call(this.press(KeyCode.RIGHT, {shift: true, ctrl: true}))
-        .expectSpeech('second', 'Heading 1', 'selected')
+      .call(doCmd('previousHeading'))
+      .expectSpeech('second', 'Heading 1')
+      .call(this.press(KeyCode.RIGHT, {shift: true, ctrl: true}))
+      .expectSpeech('second', 'Heading 1', 'selected')
 
-        .call(doCmd('nextLink'))
-        .expectSpeech('fourth', 'Internal link')
-        .call(this.press(KeyCode.RIGHT, {shift: true, ctrl: true}))
-        .expectSpeech('fourth', 'Link', 'selected')
+      .call(doCmd('nextLink'))
+      .expectSpeech('fourth', 'Internal link')
+      .call(this.press(KeyCode.RIGHT, {shift: true, ctrl: true}))
+      .expectSpeech('fourth', 'Link', 'selected')
 
-        .replay();
-  });
+      .replay();
 });
diff --git a/chrome/browser/resources/chromeos/accessibility/chromevox/background/keyboard_handler_test.js b/chrome/browser/resources/chromeos/accessibility/chromevox/background/keyboard_handler_test.js
index 5df64e9..110ae7ef 100644
--- a/chrome/browser/resources/chromeos/accessibility/chromevox/background/keyboard_handler_test.js
+++ b/chrome/browser/resources/chromeos/accessibility/chromevox/background/keyboard_handler_test.js
@@ -22,236 +22,240 @@
 
 TEST_F(
     'ChromeVoxBackgroundKeyboardHandlerTest', 'SearchGetsPassedThrough',
-    function() {
-      this.runWithLoadedTree('<p>test</p>', function() {
-        // A Search keydown gets eaten.
-        const searchDown = {};
-        searchDown.preventDefault = this.newCallback();
-        searchDown.stopPropagation = this.newCallback();
-        searchDown.metaKey = true;
-        keyboardHandler.onKeyDown(searchDown);
-        assertEquals(1, keyboardHandler.eatenKeyDowns_.size);
+    async function() {
+      await this.runWithLoadedTree('<p>test</p>');
+      // A Search keydown gets eaten.
+      const searchDown = {};
+      searchDown.preventDefault = this.newCallback();
+      searchDown.stopPropagation = this.newCallback();
+      searchDown.metaKey = true;
+      keyboardHandler.onKeyDown(searchDown);
+      assertEquals(1, keyboardHandler.eatenKeyDowns_.size);
 
-        // A Search keydown does not get eaten when there's no range and there
-        // was no previous range. TalkBack is handled elsewhere.
-        ChromeVoxState.instance.setCurrentRange(null);
-        ChromeVoxState.instance.previousRange_ = null;
-        const searchDown2 = {};
-        searchDown2.metaKey = true;
-        keyboardHandler.onKeyDown(searchDown2);
-        assertEquals(1, keyboardHandler.eatenKeyDowns_.size);
-      });
+      // A Search keydown does not get eaten when there's no range and there
+      // was no previous range. TalkBack is handled elsewhere.
+      ChromeVoxState.instance.setCurrentRange(null);
+      ChromeVoxState.instance.previousRange_ = null;
+      const searchDown2 = {};
+      searchDown2.metaKey = true;
+      keyboardHandler.onKeyDown(searchDown2);
+      assertEquals(1, keyboardHandler.eatenKeyDowns_.size);
     });
 
-TEST_F('ChromeVoxBackgroundKeyboardHandlerTest', 'PassThroughMode', function() {
-  this.runWithLoadedTree('<p>test</p>', function() {
-    assertUndefined(ChromeVox.passThroughMode);
-    assertEquals('no_pass_through', keyboardHandler.passThroughState_);
-    assertEquals(0, keyboardHandler.eatenKeyDowns_.size);
-    assertEquals(0, keyboardHandler.passedThroughKeyDowns_.size);
+TEST_F(
+    'ChromeVoxBackgroundKeyboardHandlerTest', 'PassThroughMode',
+    async function() {
+      await this.runWithLoadedTree('<p>test</p>');
+      assertUndefined(ChromeVox.passThroughMode);
+      assertEquals('no_pass_through', keyboardHandler.passThroughState_);
+      assertEquals(0, keyboardHandler.eatenKeyDowns_.size);
+      assertEquals(0, keyboardHandler.passedThroughKeyDowns_.size);
 
-    // Send the pass through command: Search+Shift+Escape.
-    const search =
-        TestUtils.createMockKeyEvent(KeyCode.SEARCH, {metaKey: true});
-    keyboardHandler.onKeyDown(search);
-    assertEquals(1, keyboardHandler.eatenKeyDowns_.size);
-    assertEquals(0, keyboardHandler.passedThroughKeyDowns_.size);
-    assertEquals('no_pass_through', keyboardHandler.passThroughState_);
-    assertUndefined(ChromeVox.passThroughMode);
+      // Send the pass through command: Search+Shift+Escape.
+      const search =
+          TestUtils.createMockKeyEvent(KeyCode.SEARCH, {metaKey: true});
+      keyboardHandler.onKeyDown(search);
+      assertEquals(1, keyboardHandler.eatenKeyDowns_.size);
+      assertEquals(0, keyboardHandler.passedThroughKeyDowns_.size);
+      assertEquals('no_pass_through', keyboardHandler.passThroughState_);
+      assertUndefined(ChromeVox.passThroughMode);
 
-    const searchShift = TestUtils.createMockKeyEvent(
-        KeyCode.SHIFT, {metaKey: true, shiftKey: true});
-    keyboardHandler.onKeyDown(searchShift);
-    assertEquals(2, keyboardHandler.eatenKeyDowns_.size);
-    assertEquals(0, keyboardHandler.passedThroughKeyDowns_.size);
-    assertEquals('no_pass_through', keyboardHandler.passThroughState_);
-    assertUndefined(ChromeVox.passThroughMode);
+      const searchShift = TestUtils.createMockKeyEvent(
+          KeyCode.SHIFT, {metaKey: true, shiftKey: true});
+      keyboardHandler.onKeyDown(searchShift);
+      assertEquals(2, keyboardHandler.eatenKeyDowns_.size);
+      assertEquals(0, keyboardHandler.passedThroughKeyDowns_.size);
+      assertEquals('no_pass_through', keyboardHandler.passThroughState_);
+      assertUndefined(ChromeVox.passThroughMode);
 
-    const searchShiftEsc = TestUtils.createMockKeyEvent(
-        KeyCode.ESCAPE, {metaKey: true, shiftKey: true});
-    keyboardHandler.onKeyDown(searchShiftEsc);
-    assertEquals(3, keyboardHandler.eatenKeyDowns_.size);
-    assertEquals(0, keyboardHandler.passedThroughKeyDowns_.size);
-    assertEquals(
-        'pending_pass_through_keyups', keyboardHandler.passThroughState_);
-    assertTrue(ChromeVox.passThroughMode);
+      const searchShiftEsc = TestUtils.createMockKeyEvent(
+          KeyCode.ESCAPE, {metaKey: true, shiftKey: true});
+      keyboardHandler.onKeyDown(searchShiftEsc);
+      assertEquals(3, keyboardHandler.eatenKeyDowns_.size);
+      assertEquals(0, keyboardHandler.passedThroughKeyDowns_.size);
+      assertEquals(
+          'pending_pass_through_keyups', keyboardHandler.passThroughState_);
+      assertTrue(ChromeVox.passThroughMode);
 
-    keyboardHandler.onKeyUp(searchShiftEsc);
-    assertEquals(2, keyboardHandler.eatenKeyDowns_.size);
-    assertEquals(0, keyboardHandler.passedThroughKeyDowns_.size);
-    assertEquals(
-        'pending_pass_through_keyups', keyboardHandler.passThroughState_);
-    assertTrue(ChromeVox.passThroughMode);
+      keyboardHandler.onKeyUp(searchShiftEsc);
+      assertEquals(2, keyboardHandler.eatenKeyDowns_.size);
+      assertEquals(0, keyboardHandler.passedThroughKeyDowns_.size);
+      assertEquals(
+          'pending_pass_through_keyups', keyboardHandler.passThroughState_);
+      assertTrue(ChromeVox.passThroughMode);
 
-    keyboardHandler.onKeyUp(searchShift);
-    assertEquals(1, keyboardHandler.eatenKeyDowns_.size);
-    assertEquals(0, keyboardHandler.passedThroughKeyDowns_.size);
-    assertEquals(
-        'pending_pass_through_keyups', keyboardHandler.passThroughState_);
-    assertTrue(ChromeVox.passThroughMode);
+      keyboardHandler.onKeyUp(searchShift);
+      assertEquals(1, keyboardHandler.eatenKeyDowns_.size);
+      assertEquals(0, keyboardHandler.passedThroughKeyDowns_.size);
+      assertEquals(
+          'pending_pass_through_keyups', keyboardHandler.passThroughState_);
+      assertTrue(ChromeVox.passThroughMode);
 
-    keyboardHandler.onKeyUp(search);
-    assertEquals(0, keyboardHandler.eatenKeyDowns_.size);
-    assertEquals(0, keyboardHandler.passedThroughKeyDowns_.size);
-    assertEquals('pending_shortcut_keyups', keyboardHandler.passThroughState_);
-    assertTrue(ChromeVox.passThroughMode);
+      keyboardHandler.onKeyUp(search);
+      assertEquals(0, keyboardHandler.eatenKeyDowns_.size);
+      assertEquals(0, keyboardHandler.passedThroughKeyDowns_.size);
+      assertEquals(
+          'pending_shortcut_keyups', keyboardHandler.passThroughState_);
+      assertTrue(ChromeVox.passThroughMode);
 
-    // Now, the next series of key downs should be passed through.
-    // Try Search+Ctrl+M.
-    keyboardHandler.onKeyDown(search);
-    assertEquals(0, keyboardHandler.eatenKeyDowns_.size);
-    assertEquals(1, keyboardHandler.passedThroughKeyDowns_.size);
-    assertEquals('pending_shortcut_keyups', keyboardHandler.passThroughState_);
-    assertTrue(ChromeVox.passThroughMode);
+      // Now, the next series of key downs should be passed through.
+      // Try Search+Ctrl+M.
+      keyboardHandler.onKeyDown(search);
+      assertEquals(0, keyboardHandler.eatenKeyDowns_.size);
+      assertEquals(1, keyboardHandler.passedThroughKeyDowns_.size);
+      assertEquals(
+          'pending_shortcut_keyups', keyboardHandler.passThroughState_);
+      assertTrue(ChromeVox.passThroughMode);
 
-    const searchCtrl = TestUtils.createMockKeyEvent(
-        KeyCode.CONTROL, {metaKey: true, ctrlKey: true});
-    keyboardHandler.onKeyDown(searchCtrl);
-    assertEquals(0, keyboardHandler.eatenKeyDowns_.size);
-    assertEquals(2, keyboardHandler.passedThroughKeyDowns_.size);
-    assertEquals('pending_shortcut_keyups', keyboardHandler.passThroughState_);
-    assertTrue(ChromeVox.passThroughMode);
+      const searchCtrl = TestUtils.createMockKeyEvent(
+          KeyCode.CONTROL, {metaKey: true, ctrlKey: true});
+      keyboardHandler.onKeyDown(searchCtrl);
+      assertEquals(0, keyboardHandler.eatenKeyDowns_.size);
+      assertEquals(2, keyboardHandler.passedThroughKeyDowns_.size);
+      assertEquals(
+          'pending_shortcut_keyups', keyboardHandler.passThroughState_);
+      assertTrue(ChromeVox.passThroughMode);
 
-    const searchCtrlM =
-        TestUtils.createMockKeyEvent(KeyCode.M, {metaKey: true, ctrlKey: true});
-    keyboardHandler.onKeyDown(searchCtrlM);
-    assertEquals(0, keyboardHandler.eatenKeyDowns_.size);
-    assertEquals(3, keyboardHandler.passedThroughKeyDowns_.size);
-    assertEquals('pending_shortcut_keyups', keyboardHandler.passThroughState_);
-    assertTrue(ChromeVox.passThroughMode);
+      const searchCtrlM = TestUtils.createMockKeyEvent(
+          KeyCode.M, {metaKey: true, ctrlKey: true});
+      keyboardHandler.onKeyDown(searchCtrlM);
+      assertEquals(0, keyboardHandler.eatenKeyDowns_.size);
+      assertEquals(3, keyboardHandler.passedThroughKeyDowns_.size);
+      assertEquals(
+          'pending_shortcut_keyups', keyboardHandler.passThroughState_);
+      assertTrue(ChromeVox.passThroughMode);
 
-    keyboardHandler.onKeyUp(searchCtrlM);
-    assertEquals(0, keyboardHandler.eatenKeyDowns_.size);
-    assertEquals(2, keyboardHandler.passedThroughKeyDowns_.size);
-    assertEquals('pending_shortcut_keyups', keyboardHandler.passThroughState_);
-    assertTrue(ChromeVox.passThroughMode);
+      keyboardHandler.onKeyUp(searchCtrlM);
+      assertEquals(0, keyboardHandler.eatenKeyDowns_.size);
+      assertEquals(2, keyboardHandler.passedThroughKeyDowns_.size);
+      assertEquals(
+          'pending_shortcut_keyups', keyboardHandler.passThroughState_);
+      assertTrue(ChromeVox.passThroughMode);
 
-    keyboardHandler.onKeyUp(searchCtrl);
-    assertEquals(0, keyboardHandler.eatenKeyDowns_.size);
-    assertEquals(1, keyboardHandler.passedThroughKeyDowns_.size);
-    assertEquals('pending_shortcut_keyups', keyboardHandler.passThroughState_);
-    assertTrue(ChromeVox.passThroughMode);
+      keyboardHandler.onKeyUp(searchCtrl);
+      assertEquals(0, keyboardHandler.eatenKeyDowns_.size);
+      assertEquals(1, keyboardHandler.passedThroughKeyDowns_.size);
+      assertEquals(
+          'pending_shortcut_keyups', keyboardHandler.passThroughState_);
+      assertTrue(ChromeVox.passThroughMode);
 
-    keyboardHandler.onKeyUp(search);
-    assertEquals(0, keyboardHandler.eatenKeyDowns_.size);
-    assertEquals(0, keyboardHandler.passedThroughKeyDowns_.size);
-    assertEquals('no_pass_through', keyboardHandler.passThroughState_);
-    assertFalse(ChromeVox.passThroughMode);
-  });
-});
+      keyboardHandler.onKeyUp(search);
+      assertEquals(0, keyboardHandler.eatenKeyDowns_.size);
+      assertEquals(0, keyboardHandler.passedThroughKeyDowns_.size);
+      assertEquals('no_pass_through', keyboardHandler.passThroughState_);
+      assertFalse(ChromeVox.passThroughMode);
+    });
 
 TEST_F(
-    'ChromeVoxBackgroundKeyboardHandlerTest', 'PassThroughModeOff', function() {
-      this.runWithLoadedTree('<p>test</p>', function() {
-        function assertNoPassThrough() {
-          assertUndefined(ChromeVox.passThroughMode);
-          assertEquals('no_pass_through', keyboardHandler.passThroughState_);
-          assertEquals(0, keyboardHandler.passedThroughKeyDowns_.size);
-        }
+    'ChromeVoxBackgroundKeyboardHandlerTest', 'PassThroughModeOff',
+    async function() {
+      await this.runWithLoadedTree('<p>test</p>');
+      function assertNoPassThrough() {
+        assertUndefined(ChromeVox.passThroughMode);
+        assertEquals('no_pass_through', keyboardHandler.passThroughState_);
+        assertEquals(0, keyboardHandler.passedThroughKeyDowns_.size);
+      }
 
-        // Send some random keys; ensure the pass through state variables never
-        // change.
-        const search =
-            TestUtils.createMockKeyEvent(KeyCode.SEARCH, {metaKey: true});
-        keyboardHandler.onKeyDown(search);
-        assertNoPassThrough();
+      // Send some random keys; ensure the pass through state variables never
+      // change.
+      const search =
+          TestUtils.createMockKeyEvent(KeyCode.SEARCH, {metaKey: true});
+      keyboardHandler.onKeyDown(search);
+      assertNoPassThrough();
 
-        const searchShift = TestUtils.createMockKeyEvent(
-            KeyCode.SHIFT, {metaKey: true, shiftKey: true});
-        keyboardHandler.onKeyDown(searchShift);
-        assertNoPassThrough();
+      const searchShift = TestUtils.createMockKeyEvent(
+          KeyCode.SHIFT, {metaKey: true, shiftKey: true});
+      keyboardHandler.onKeyDown(searchShift);
+      assertNoPassThrough();
 
-        const searchShiftM = TestUtils.createMockKeyEvent(
-            KeyCode.M, {metaKey: true, shiftKey: true});
-        keyboardHandler.onKeyDown(searchShiftM);
-        assertNoPassThrough();
+      const searchShiftM = TestUtils.createMockKeyEvent(
+          KeyCode.M, {metaKey: true, shiftKey: true});
+      keyboardHandler.onKeyDown(searchShiftM);
+      assertNoPassThrough();
 
-        keyboardHandler.onKeyUp(searchShiftM);
-        assertNoPassThrough();
+      keyboardHandler.onKeyUp(searchShiftM);
+      assertNoPassThrough();
 
-        keyboardHandler.onKeyUp(searchShift);
-        assertNoPassThrough();
+      keyboardHandler.onKeyUp(searchShift);
+      assertNoPassThrough();
 
-        keyboardHandler.onKeyUp(search);
-        assertNoPassThrough();
+      keyboardHandler.onKeyUp(search);
+      assertNoPassThrough();
 
-        keyboardHandler.onKeyDown(TestUtils.createMockKeyEvent(KeyCode.A));
-        assertNoPassThrough();
+      keyboardHandler.onKeyDown(TestUtils.createMockKeyEvent(KeyCode.A));
+      assertNoPassThrough();
 
-        keyboardHandler.onKeyDown(
-            TestUtils.createMockKeyEvent(KeyCode.A, {altKey: true}));
-        assertNoPassThrough();
+      keyboardHandler.onKeyDown(
+          TestUtils.createMockKeyEvent(KeyCode.A, {altKey: true}));
+      assertNoPassThrough();
 
-        keyboardHandler.onKeyUp(
-            TestUtils.createMockKeyEvent(KeyCode.A, {altKey: true}));
-        assertNoPassThrough();
-      });
+      keyboardHandler.onKeyUp(
+          TestUtils.createMockKeyEvent(KeyCode.A, {altKey: true}));
+      assertNoPassThrough();
     });
 
 TEST_F(
     'ChromeVoxBackgroundKeyboardHandlerTest', 'UnexpectedKeyDownUpPairs',
-    function() {
-      this.runWithLoadedTree('<p>test</p>', function() {
-        // Send a few key downs.
-        const search =
-            TestUtils.createMockKeyEvent(KeyCode.SEARCH, {metaKey: true});
-        keyboardHandler.onKeyDown(search);
-        assertEquals(1, keyboardHandler.eatenKeyDowns_.size);
+    async function() {
+      await this.runWithLoadedTree('<p>test</p>');
+      // Send a few key downs.
+      const search =
+          TestUtils.createMockKeyEvent(KeyCode.SEARCH, {metaKey: true});
+      keyboardHandler.onKeyDown(search);
+      assertEquals(1, keyboardHandler.eatenKeyDowns_.size);
 
-        const searchShift = TestUtils.createMockKeyEvent(
-            KeyCode.SHIFT, {metaKey: true, shiftKey: true});
-        keyboardHandler.onKeyDown(searchShift);
-        assertEquals(2, keyboardHandler.eatenKeyDowns_.size);
+      const searchShift = TestUtils.createMockKeyEvent(
+          KeyCode.SHIFT, {metaKey: true, shiftKey: true});
+      keyboardHandler.onKeyDown(searchShift);
+      assertEquals(2, keyboardHandler.eatenKeyDowns_.size);
 
-        const searchShiftM = TestUtils.createMockKeyEvent(
-            KeyCode.M, {metaKey: true, shiftKey: true});
-        keyboardHandler.onKeyDown(searchShiftM);
-        assertEquals(3, keyboardHandler.eatenKeyDowns_.size);
+      const searchShiftM = TestUtils.createMockKeyEvent(
+          KeyCode.M, {metaKey: true, shiftKey: true});
+      keyboardHandler.onKeyDown(searchShiftM);
+      assertEquals(3, keyboardHandler.eatenKeyDowns_.size);
 
-        // Now, send a key down, but no modifiers set, which is impossible to
-        // actually press. This key is not eaten.
-        const m = TestUtils.createMockKeyEvent(KeyCode.M, {});
-        keyboardHandler.onKeyDown(m);
-        assertEquals(0, keyboardHandler.eatenKeyDowns_.size);
+      // Now, send a key down, but no modifiers set, which is impossible to
+      // actually press. This key is not eaten.
+      const m = TestUtils.createMockKeyEvent(KeyCode.M, {});
+      keyboardHandler.onKeyDown(m);
+      assertEquals(0, keyboardHandler.eatenKeyDowns_.size);
 
-        // To demonstrate eaten keys still work, send Search by itself, which is
-        // always eaten.
-        keyboardHandler.onKeyDown(search);
-        assertEquals(1, keyboardHandler.eatenKeyDowns_.size);
-      });
+      // To demonstrate eaten keys still work, send Search by itself, which is
+      // always eaten.
+      keyboardHandler.onKeyDown(search);
+      assertEquals(1, keyboardHandler.eatenKeyDowns_.size);
     });
 
 TEST_F(
     'ChromeVoxBackgroundKeyboardHandlerTest',
-    'UnexpectedKeyDownUpPairsPassThrough', function() {
-      this.runWithLoadedTree('<p>test</p>', function() {
-        // Force pass through mode.
-        ChromeVox.passThroughMode = true;
+    'UnexpectedKeyDownUpPairsPassThrough', async function() {
+      await this.runWithLoadedTree('<p>test</p>');
+      // Force pass through mode.
+      ChromeVox.passThroughMode = true;
 
-        // Send a few key downs (which are passed through).
-        const search =
-            TestUtils.createMockKeyEvent(KeyCode.SEARCH, {metaKey: true});
-        keyboardHandler.onKeyDown(search);
-        assertEquals(0, keyboardHandler.eatenKeyDowns_.size);
-        assertEquals(1, keyboardHandler.passedThroughKeyDowns_.size);
+      // Send a few key downs (which are passed through).
+      const search =
+          TestUtils.createMockKeyEvent(KeyCode.SEARCH, {metaKey: true});
+      keyboardHandler.onKeyDown(search);
+      assertEquals(0, keyboardHandler.eatenKeyDowns_.size);
+      assertEquals(1, keyboardHandler.passedThroughKeyDowns_.size);
 
-        const searchShift = TestUtils.createMockKeyEvent(
-            KeyCode.SHIFT, {metaKey: true, shiftKey: true});
-        keyboardHandler.onKeyDown(searchShift);
-        assertEquals(0, keyboardHandler.eatenKeyDowns_.size);
-        assertEquals(2, keyboardHandler.passedThroughKeyDowns_.size);
+      const searchShift = TestUtils.createMockKeyEvent(
+          KeyCode.SHIFT, {metaKey: true, shiftKey: true});
+      keyboardHandler.onKeyDown(searchShift);
+      assertEquals(0, keyboardHandler.eatenKeyDowns_.size);
+      assertEquals(2, keyboardHandler.passedThroughKeyDowns_.size);
 
-        const searchShiftM = TestUtils.createMockKeyEvent(
-            KeyCode.M, {metaKey: true, shiftKey: true});
-        keyboardHandler.onKeyDown(searchShiftM);
-        assertEquals(0, keyboardHandler.eatenKeyDowns_.size);
-        assertEquals(3, keyboardHandler.passedThroughKeyDowns_.size);
+      const searchShiftM = TestUtils.createMockKeyEvent(
+          KeyCode.M, {metaKey: true, shiftKey: true});
+      keyboardHandler.onKeyDown(searchShiftM);
+      assertEquals(0, keyboardHandler.eatenKeyDowns_.size);
+      assertEquals(3, keyboardHandler.passedThroughKeyDowns_.size);
 
-        // Now, send a key down, but no modifiers set, which is impossible to
-        // actually press. This is passed through, so the count resets to 1.
-        const m = TestUtils.createMockKeyEvent(KeyCode.M, {});
-        keyboardHandler.onKeyDown(m);
-        assertEquals(0, keyboardHandler.eatenKeyDowns_.size);
-        assertEquals(1, keyboardHandler.passedThroughKeyDowns_.size);
-      });
+      // Now, send a key down, but no modifiers set, which is impossible to
+      // actually press. This is passed through, so the count resets to 1.
+      const m = TestUtils.createMockKeyEvent(KeyCode.M, {});
+      keyboardHandler.onKeyDown(m);
+      assertEquals(0, keyboardHandler.eatenKeyDowns_.size);
+      assertEquals(1, keyboardHandler.passedThroughKeyDowns_.size);
     });
diff --git a/chrome/browser/resources/chromeos/accessibility/chromevox/background/keymaps/key_map.js b/chrome/browser/resources/chromeos/accessibility/chromevox/background/keymaps/key_map.js
index 59acae8..c10847fd 100644
--- a/chrome/browser/resources/chromeos/accessibility/chromevox/background/keymaps/key_map.js
+++ b/chrome/browser/resources/chromeos/accessibility/chromevox/background/keymaps/key_map.js
@@ -20,14 +20,9 @@
  * UserCommands.
  */
 
-goog.provide('KeyMap');
+// TODO(dtseng): KeyUtil only needed for sticky mode.
 
-goog.require('KeyCode');
-
-// TODO(dtseng): Only needed for sticky mode.
-goog.require('KeyUtil');
-
-KeyMap = class {
+export class KeyMap {
   /**
    * @param {Array<Object<{command: string, sequence: KeySequence}>>}
    * commandsAndKeySequences An array of pairs - KeySequences and commands.
@@ -212,7 +207,7 @@
       this.commandToKey_[binding.command] = binding.sequence;
     }
   }
-};
+}
 
 // This is intentionally not type-checked, as it is a serialized set of
 // KeySequence objects.
diff --git a/chrome/browser/resources/chromeos/accessibility/chromevox/background/live_regions_test.js b/chrome/browser/resources/chromeos/accessibility/chromevox/background/live_regions_test.js
index 703f9fd..591d9845 100644
--- a/chrome/browser/resources/chromeos/accessibility/chromevox/background/live_regions_test.js
+++ b/chrome/browser/resources/chromeos/accessibility/chromevox/background/live_regions_test.js
@@ -28,10 +28,9 @@
 };
 
 
-TEST_F('ChromeVoxLiveRegionsTest', 'LiveRegionAddElement', function() {
+TEST_F('ChromeVoxLiveRegionsTest', 'LiveRegionAddElement', async function() {
   const mockFeedback = this.createMockFeedback();
-  this.runWithLoadedTree(
-      `
+  const rootNode = await this.runWithLoadedTree(`
       <h1>Document with live region</h1>
       <p id="live" aria-live="assertive"></p>
       <button id="go">Go</button>
@@ -40,19 +39,16 @@
           document.getElementById('live').innerHTML = 'Hello, world';
         }, false);
       </script>
-    `,
-      function(rootNode) {
-        const go = rootNode.find({role: RoleType.BUTTON});
-        mockFeedback.call(go.doDefault.bind(go))
-            .expectCategoryFlushSpeech('Hello, world');
-        mockFeedback.replay();
-      });
+    `);
+  const go = rootNode.find({role: RoleType.BUTTON});
+  mockFeedback.call(go.doDefault.bind(go))
+      .expectCategoryFlushSpeech('Hello, world');
+  mockFeedback.replay();
 });
 
-TEST_F('ChromeVoxLiveRegionsTest', 'LiveRegionRemoveElement', function() {
+TEST_F('ChromeVoxLiveRegionsTest', 'LiveRegionRemoveElement', async function() {
   const mockFeedback = this.createMockFeedback();
-  this.runWithLoadedTree(
-      `
+  const rootNode = await this.runWithLoadedTree(`
       <h1>Document with live region</h1>
       <p id="live" aria-live="assertive" aria-relevant="removals">Hello, world</p>
       <button id="go">Go</button>
@@ -61,21 +57,18 @@
           document.getElementById('live').innerHTML = '';
         }, false);
       </script>
-    `,
-      function(rootNode) {
-        const go = rootNode.find({role: RoleType.BUTTON});
-        go.doDefault();
-        mockFeedback.expectCategoryFlushSpeech('removed:')
-            .expectQueuedSpeech('Hello, world');
-        mockFeedback.replay();
-      });
+    `);
+  const go = rootNode.find({role: RoleType.BUTTON});
+  go.doDefault();
+  mockFeedback.expectCategoryFlushSpeech('removed:')
+      .expectQueuedSpeech('Hello, world');
+  mockFeedback.replay();
 });
 
-TEST_F('ChromeVoxLiveRegionsTest', 'LiveRegionChangeAtomic', function() {
+TEST_F('ChromeVoxLiveRegionsTest', 'LiveRegionChangeAtomic', async function() {
   LiveRegions.LIVE_REGION_QUEUE_TIME_MS = 0;
   const mockFeedback = this.createMockFeedback();
-  this.runWithLoadedTree(
-      `
+  const rootNode = await this.runWithLoadedTree(`
       <div id="live" aria-live="assertive" aria-atomic="true">
         <div></div><div>Bravo</div><div></div>
       </div>
@@ -86,20 +79,18 @@
           document.querySelectorAll('div div')[0].textContent = 'Alpha';
         }, false);
       </script>
-    `,
-      function(rootNode) {
-        const go = rootNode.find({role: RoleType.BUTTON});
-        mockFeedback.call(go.doDefault.bind(go))
-            .expectCategoryFlushSpeech('Alpha Bravo Charlie');
-        mockFeedback.replay();
-      });
+    `);
+  const go = rootNode.find({role: RoleType.BUTTON});
+  mockFeedback.call(go.doDefault.bind(go))
+      .expectCategoryFlushSpeech('Alpha Bravo Charlie');
+  mockFeedback.replay();
 });
 
-TEST_F('ChromeVoxLiveRegionsTest', 'LiveRegionChangeAtomicText', function() {
-  LiveRegions.LIVE_REGION_QUEUE_TIME_MS = 0;
-  const mockFeedback = this.createMockFeedback();
-  this.runWithLoadedTree(
-      `
+TEST_F(
+    'ChromeVoxLiveRegionsTest', 'LiveRegionChangeAtomicText', async function() {
+      LiveRegions.LIVE_REGION_QUEUE_TIME_MS = 0;
+      const mockFeedback = this.createMockFeedback();
+      const rootNode = await this.runWithLoadedTree(`
       <h1 aria-atomic="true" id="live"aria-live="assertive">foo</h1>
       <button id="go">go</button>
       <script>
@@ -107,25 +98,23 @@
           document.getElementById('live').innerText = 'bar';
         });
       </script>
-    `,
-      function(rootNode) {
-        const go = rootNode.find({role: RoleType.BUTTON});
-        mockFeedback.call(go.doDefault.bind(go))
-            .expectCategoryFlushSpeech('bar', 'Heading 1');
-        mockFeedback.replay();
-      });
-});
+    `);
+      const go = rootNode.find({role: RoleType.BUTTON});
+      mockFeedback.call(go.doDefault.bind(go))
+          .expectCategoryFlushSpeech('bar', 'Heading 1');
+      mockFeedback.replay();
+    });
 
-TEST_F('ChromeVoxLiveRegionsTest', 'LiveRegionChangeImageAlt', function() {
-  // Note that there is a live region outputted as a result of page load; the
-  // test expects a live region announcement after a click on the button, but
-  // the LiveRegions module has a half second filter for live region
-  // announcements on the same node. Set that timeout to 0 to prevent
-  // flakeyness.
-  LiveRegions.LIVE_REGION_QUEUE_TIME_MS = 0;
-  const mockFeedback = this.createMockFeedback();
-  this.runWithLoadedTree(
-      `
+TEST_F(
+    'ChromeVoxLiveRegionsTest', 'LiveRegionChangeImageAlt', async function() {
+      // Note that there is a live region outputted as a result of page load;
+      // the test expects a live region announcement after a click on the
+      // button, but the LiveRegions module has a half second filter for live
+      // region announcements on the same node. Set that timeout to 0 to prevent
+      // flakeyness.
+      LiveRegions.LIVE_REGION_QUEUE_TIME_MS = 0;
+      const mockFeedback = this.createMockFeedback();
+      const rootNode = await this.runWithLoadedTree(`
       <div id="live" aria-live="assertive">
         <img id="img" src="#" alt="Before">
       </div>
@@ -135,19 +124,16 @@
           document.getElementById('img').setAttribute('alt', 'After');
         }, false);
       </script>
-    `,
-      function(rootNode) {
-        const go = rootNode.find({role: RoleType.BUTTON});
-        mockFeedback.call(go.doDefault.bind(go))
-            .expectCategoryFlushSpeech('After');
-        mockFeedback.replay();
-      });
-});
+    `);
+      const go = rootNode.find({role: RoleType.BUTTON});
+      mockFeedback.call(go.doDefault.bind(go))
+          .expectCategoryFlushSpeech('After');
+      mockFeedback.replay();
+    });
 
-TEST_F('ChromeVoxLiveRegionsTest', 'LiveRegionThenFocus', function() {
+TEST_F('ChromeVoxLiveRegionsTest', 'LiveRegionThenFocus', async function() {
   const mockFeedback = this.createMockFeedback();
-  this.runWithLoadedTree(
-      `
+  const rootNode = await this.runWithLoadedTree(`
       <div id="live" aria-live="assertive"></div>
       <button id="go">Go</button>
       <button id="focus">Focus</button>
@@ -159,38 +145,35 @@
           }, 50);
         }, false);
       </script>
-    `,
-      function(rootNode) {
-        // Due to the above timing component, the live region can come either
-        // before or after the focus output. This depends on the EventBundle to
-        // which we get the live region. It can either be in its own bundle or
-        // be part of the bundle with the focus change. In either case, the
-        // first event should be flushed; the second should either be queued (in
-        // the case of the focus) or category flushed for the live region.
-        let sawFocus = false;
-        let sawLive = false;
-        const focusOrLive = function(candidate) {
-          sawFocus = candidate.text === 'Focus' || sawFocus;
-          sawLive = candidate.text === 'Live' || sawLive;
-          if (sawFocus && sawLive) {
-            return candidate.queueMode !== QueueMode.FLUSH;
-          } else if (sawFocus || sawLive) {
-            return candidate.queueMode === QueueMode.FLUSH;
-          }
-        };
-        const go = rootNode.find({role: RoleType.BUTTON});
-        mockFeedback.call(this.simulateUserInteraction.bind(this))
-            .call(go.doDefault.bind(go))
-            .expectSpeech(focusOrLive)
-            .expectSpeech(focusOrLive);
-        mockFeedback.replay();
-      });
+    `);
+  // Due to the above timing component, the live region can come either
+  // before or after the focus output. This depends on the EventBundle to
+  // which we get the live region. It can either be in its own bundle or
+  // be part of the bundle with the focus change. In either case, the
+  // first event should be flushed; the second should either be queued (in
+  // the case of the focus) or category flushed for the live region.
+  let sawFocus = false;
+  let sawLive = false;
+  const focusOrLive = function(candidate) {
+    sawFocus = candidate.text === 'Focus' || sawFocus;
+    sawLive = candidate.text === 'Live' || sawLive;
+    if (sawFocus && sawLive) {
+      return candidate.queueMode !== QueueMode.FLUSH;
+    } else if (sawFocus || sawLive) {
+      return candidate.queueMode === QueueMode.FLUSH;
+    }
+  };
+  const go = rootNode.find({role: RoleType.BUTTON});
+  mockFeedback.call(this.simulateUserInteraction.bind(this))
+      .call(go.doDefault.bind(go))
+      .expectSpeech(focusOrLive)
+      .expectSpeech(focusOrLive);
+  mockFeedback.replay();
 });
 
-TEST_F('ChromeVoxLiveRegionsTest', 'FocusThenLiveRegion', function() {
+TEST_F('ChromeVoxLiveRegionsTest', 'FocusThenLiveRegion', async function() {
   const mockFeedback = this.createMockFeedback();
-  this.runWithLoadedTree(
-      `
+  const rootNode = await this.runWithLoadedTree(`
       <div id="live" aria-live="assertive"></div>
       <button id="go">Go</button>
       <button id="focus">Focus</button>
@@ -202,28 +185,25 @@
           }, 200);
         }, false);
       </script>
-    `,
-      function(rootNode) {
-        const go = rootNode.find({role: RoleType.BUTTON});
-        mockFeedback.call(this.simulateUserInteraction.bind(this))
-            .call(go.doDefault.bind(go))
-            .expectSpeech('Focus')
-            .expectSpeech((candidate) => {
-              return candidate.text === 'Live' &&
-                  (candidate.queueMode === QueueMode.CATEGORY_FLUSH ||
-                   candidate.queueMode === QueueMode.QUEUE);
-            });
-        mockFeedback.replay();
+    `);
+  const go = rootNode.find({role: RoleType.BUTTON});
+  mockFeedback.call(this.simulateUserInteraction.bind(this))
+      .call(go.doDefault.bind(go))
+      .expectSpeech('Focus')
+      .expectSpeech((candidate) => {
+        return candidate.text === 'Live' &&
+            (candidate.queueMode === QueueMode.CATEGORY_FLUSH ||
+             candidate.queueMode === QueueMode.QUEUE);
       });
+  mockFeedback.replay();
 });
 
-TEST_F('ChromeVoxLiveRegionsTest', 'LiveRegionCategoryFlush', function() {
+TEST_F('ChromeVoxLiveRegionsTest', 'LiveRegionCategoryFlush', async function() {
   // Adjust the live region queue time to be shorter (i.e. flushes happen for
   // live regions coming 1 ms in time). Also, can help with flakeyness.
   LiveRegions.LIVE_REGION_QUEUE_TIME_MS = 1;
   const mockFeedback = this.createMockFeedback();
-  this.runWithLoadedTree(
-      `
+  const rootNode = await this.runWithLoadedTree(`
       <div id="live1" aria-live="assertive"></div>
       <div id="live2" aria-live="assertive"></div>
       <button id="go">Go</button>
@@ -236,20 +216,17 @@
           }, 1000);
         }, false);
       </script>
-    `,
-      function(rootNode) {
-        const go = rootNode.find({role: RoleType.BUTTON});
-        mockFeedback.call(go.doDefault.bind(go))
-            .expectCategoryFlushSpeech('Live1')
-            .expectCategoryFlushSpeech('Live2');
-        mockFeedback.replay();
-      });
+    `);
+  const go = rootNode.find({role: RoleType.BUTTON});
+  mockFeedback.call(go.doDefault.bind(go))
+      .expectCategoryFlushSpeech('Live1')
+      .expectCategoryFlushSpeech('Live2');
+  mockFeedback.replay();
 });
 
-TEST_F('ChromeVoxLiveRegionsTest', 'SilentOnNodeChange', function() {
+TEST_F('ChromeVoxLiveRegionsTest', 'SilentOnNodeChange', async function() {
   const mockFeedback = this.createMockFeedback();
-  this.runWithLoadedTree(
-      `
+  const rootNode = await this.runWithLoadedTree(`
     <p>start</p>
     <button>first</button>
     <div role="button" id="live" aria-live="assertive">
@@ -263,62 +240,53 @@
         pressed = !pressed;
       }, 50);
     </script>
-  `,
-      function(root) {
-        const focusAfterNodeChange = window.setTimeout.bind(window, function() {
-          root.firstChild.nextSibling.focus();
-        }, 1000);
-        mockFeedback.call(focusAfterNodeChange)
-            .expectSpeech('hello!')
-            .expectNextSpeechUtteranceIsNot('hello!')
-            .expectNextSpeechUtteranceIsNot('hello!');
-        mockFeedback.replay();
-      });
+  `);
+  const focusAfterNodeChange = window.setTimeout.bind(window, function() {
+    root.firstChild.nextSibling.focus();
+  }, 1000);
+  mockFeedback.call(focusAfterNodeChange)
+      .expectSpeech('hello!')
+      .expectNextSpeechUtteranceIsNot('hello!')
+      .expectNextSpeechUtteranceIsNot('hello!');
+  mockFeedback.replay();
 });
 
-TEST_F('ChromeVoxLiveRegionsTest', 'SimulateTreeChanges', function() {
+TEST_F('ChromeVoxLiveRegionsTest', 'SimulateTreeChanges', async function() {
   const mockFeedback = this.createMockFeedback();
-  this.runWithLoadedTree(
-      `
+  const root = await this.runWithLoadedTree(`
     <button></button>
     <div aria-live="assertive">
       <p>hello</p><p>there</p>
     </div>
-  `,
-      function(root) {
-        const live = new LiveRegions(ChromeVoxState.instance);
-        const [t1, t2] = root.findAll({role: RoleType.STATIC_TEXT});
-        mockFeedback.expectSpeech('hello there')
-            .clearPendingOutput()
-            .call(function() {
-              live.onTreeChange(
-                  {type: TreeChangeType.TEXT_CHANGED, target: t2});
-              live.onTreeChange(
-                  {type: TreeChangeType.SUBTREE_UPDATE_END, target: t2});
-            })
-            .expectNextSpeechUtteranceIsNot('hello')
-            .expectSpeech('there')
-            .clearPendingOutput();
-        mockFeedback
-            .call(function() {
-              live.onTreeChange(
-                  {type: TreeChangeType.TEXT_CHANGED, target: t1});
-              live.onTreeChange(
-                  {type: TreeChangeType.TEXT_CHANGED, target: t2});
-              live.onTreeChange(
-                  {type: TreeChangeType.SUBTREE_UPDATE_END, target: t2});
-            })
-            .expectSpeech('hello')
-            .expectSpeech('there');
-        mockFeedback.replay();
-      });
+  `);
+  const live = new LiveRegions(ChromeVoxState.instance);
+  const [t1, t2] = root.findAll({role: RoleType.STATIC_TEXT});
+  mockFeedback.expectSpeech('hello there')
+      .clearPendingOutput()
+      .call(function() {
+        live.onTreeChange({type: TreeChangeType.TEXT_CHANGED, target: t2});
+        live.onTreeChange(
+            {type: TreeChangeType.SUBTREE_UPDATE_END, target: t2});
+      })
+      .expectNextSpeechUtteranceIsNot('hello')
+      .expectSpeech('there')
+      .clearPendingOutput();
+  mockFeedback
+      .call(function() {
+        live.onTreeChange({type: TreeChangeType.TEXT_CHANGED, target: t1});
+        live.onTreeChange({type: TreeChangeType.TEXT_CHANGED, target: t2});
+        live.onTreeChange(
+            {type: TreeChangeType.SUBTREE_UPDATE_END, target: t2});
+      })
+      .expectSpeech('hello')
+      .expectSpeech('there');
+  mockFeedback.replay();
 });
 
 // Flaky: https://crbug.com/945199
-TEST_F('ChromeVoxLiveRegionsTest', 'DISABLED_LiveStatusOff', function() {
+TEST_F('ChromeVoxLiveRegionsTest', 'DISABLED_LiveStatusOff', async function() {
   const mockFeedback = this.createMockFeedback();
-  this.runWithLoadedTree(
-      `
+  const rootNode = await this.runWithLoadedTree(`
     <div><input aria-live="off" type="text"></input></div>
     <script>
       let input = document.querySelector('input');
@@ -337,25 +305,22 @@
         }
       });
     </script>
-  `,
-      function(root) {
-        const input = root.find({role: RoleType.TEXT_FIELD});
-        const clickInput = input.parent.doDefault.bind(input.parent);
-        mockFeedback.call(input.focus.bind(input))
-            .call(clickInput)
-            .expectSpeech('bb')
-            .clearPendingOutput()
-            .call(clickInput)
-            .expectNextSpeechUtteranceIsNot('bba')
-            .expectSpeech('a')
-            .replay();
-      });
+  `);
+  const input = root.find({role: RoleType.TEXT_FIELD});
+  const clickInput = input.parent.doDefault.bind(input.parent);
+  mockFeedback.call(input.focus.bind(input))
+      .call(clickInput)
+      .expectSpeech('bb')
+      .clearPendingOutput()
+      .call(clickInput)
+      .expectNextSpeechUtteranceIsNot('bba')
+      .expectSpeech('a')
+      .replay();
 });
 
-TEST_F('ChromeVoxLiveRegionsTest', 'TreeChangeOnIgnoredNode', function() {
+TEST_F('ChromeVoxLiveRegionsTest', 'TreeChangeOnIgnoredNode', async function() {
   const mockFeedback = this.createMockFeedback();
-  this.runWithLoadedTree(
-      `
+  const root = await this.runWithLoadedTree(`
     <button></button>
     <script>
       const button = document.body.children[0];
@@ -369,14 +334,13 @@
         document.body.appendChild(ignored);
       });
     </script>
-  `,
-      function(root) {
-        const button = root.find({role: chrome.automation.RoleType.BUTTON});
-        mockFeedback.call(button.doDefault.bind(button))
-            .expectSpeech('Alert', 'hi')
-            .replay();
-      });
+  `);
+  const button = root.find({role: chrome.automation.RoleType.BUTTON});
+  mockFeedback.call(button.doDefault.bind(button))
+      .expectSpeech('Alert', 'hi')
+      .replay();
 });
+
 SYNC_TEST_F('ChromeVoxLiveRegionsTest', 'ShouldIgnoreLiveRegion', function() {
   const liveRegions = new LiveRegions(ChromeVoxState.instance);
 
diff --git a/chrome/browser/resources/chromeos/accessibility/chromevox/background/loader.js b/chrome/browser/resources/chromeos/accessibility/chromevox/background/loader.js
index 77fa89c0..ef8e326 100644
--- a/chrome/browser/resources/chromeos/accessibility/chromevox/background/loader.js
+++ b/chrome/browser/resources/chromeos/accessibility/chromevox/background/loader.js
@@ -23,7 +23,6 @@
 goog.require('ChromeVoxState');
 goog.require('ChromeVoxStateObserver');
 goog.require('CommandHandlerInterface');
-goog.require('CommandStore');
 goog.require('ConsoleTts');
 goog.require('EventGenerator');
 goog.require('EventSourceState');
@@ -31,7 +30,6 @@
 goog.require('ExtensionBridge');
 goog.require('JaPhoneticMap');
 goog.require('KeyCode');
-goog.require('KeyMap');
 goog.require('KeySequence');
 goog.require('KeyUtil');
 goog.require('LibLouis.FormType');
diff --git a/chrome/browser/resources/chromeos/accessibility/chromevox/background/output/locale_output_helper_test.js b/chrome/browser/resources/chromeos/accessibility/chromevox/background/output/locale_output_helper_test.js
index a75533e..168a0d0 100644
--- a/chrome/browser/resources/chromeos/accessibility/chromevox/background/output/locale_output_helper_test.js
+++ b/chrome/browser/resources/chromeos/accessibility/chromevox/background/output/locale_output_helper_test.js
@@ -178,402 +178,393 @@
 
 TEST_F(
     'ChromeVoxLocaleOutputHelperTest', 'MultipleLanguagesLabeledDocTest',
-    function() {
+    async function() {
       const mockFeedback = this.createMockFeedback();
-      this.runWithLoadedTree(this.multipleLanguagesLabeledDoc, function() {
-        localStorage['languageSwitching'] = 'true';
-        this.setAvailableVoices();
-        mockFeedback.call(doCmd('jumpToTop'))
-            .expectSpeechWithLocale('es', 'español: Hola.');
-        mockFeedback.call(doCmd('nextLine'))
-            .expectSpeechWithLocale('en', 'English: Hello.');
-        mockFeedback.call(doCmd('nextLine'))
-            .expectSpeechWithLocale('fr', 'français: Salut.');
-        mockFeedback.call(doCmd('nextLine'))
-            .expectSpeechWithLocale('it', 'italiano: Ciao amico.');
-        mockFeedback.replay();
-      });
+      await this.runWithLoadedTree(this.multipleLanguagesLabeledDoc);
+      localStorage['languageSwitching'] = 'true';
+      this.setAvailableVoices();
+      mockFeedback.call(doCmd('jumpToTop'))
+          .expectSpeechWithLocale('es', 'español: Hola.');
+      mockFeedback.call(doCmd('nextLine'))
+          .expectSpeechWithLocale('en', 'English: Hello.');
+      mockFeedback.call(doCmd('nextLine'))
+          .expectSpeechWithLocale('fr', 'français: Salut.');
+      mockFeedback.call(doCmd('nextLine'))
+          .expectSpeechWithLocale('it', 'italiano: Ciao amico.');
+      mockFeedback.replay();
     });
 
 TEST_F(
     'ChromeVoxLocaleOutputHelperTest', 'NestedLanguagesLabeledDocTest',
-    function() {
+    async function() {
       const mockFeedback = this.createMockFeedback();
-      this.runWithLoadedTree(this.nestedLanguagesLabeledDoc, function() {
-        localStorage['languageSwitching'] = 'true';
-        this.setAvailableVoices();
-        mockFeedback.call(doCmd('jumpToTop'))
-            .expectSpeechWithLocale(
-                'en', 'In the morning, I sometimes eat breakfast.');
-        mockFeedback.call(doCmd('nextLine'))
-            .expectSpeechWithLocale(
-                'fr', 'français: Dans l\'apres-midi, je dejeune.');
-        mockFeedback.call(doCmd('nextLine'))
-            .expectSpeechWithLocale(
-                'en', 'English: Hello it\'s a pleasure to meet you. ');
-        mockFeedback.call(doCmd('nextLine'))
-            .expectSpeechWithLocale('fr', 'français: Comment ca va?');
-        mockFeedback.call(doCmd('nextLine'))
-            .expectSpeechWithLocale(
-                'en', 'English: Switching back to English. ');
-        mockFeedback.call(doCmd('nextLine'))
-            .expectSpeechWithLocale('es', 'español: Hola.');
-        mockFeedback.call(doCmd('nextLine'))
-            .expectSpeechWithLocale('en', 'English: Goodbye.');
-        mockFeedback.replay();
-      });
+      await this.runWithLoadedTree(this.nestedLanguagesLabeledDoc);
+      localStorage['languageSwitching'] = 'true';
+      this.setAvailableVoices();
+      mockFeedback.call(doCmd('jumpToTop'))
+          .expectSpeechWithLocale(
+              'en', 'In the morning, I sometimes eat breakfast.');
+      mockFeedback.call(doCmd('nextLine'))
+          .expectSpeechWithLocale(
+              'fr', 'français: Dans l\'apres-midi, je dejeune.');
+      mockFeedback.call(doCmd('nextLine'))
+          .expectSpeechWithLocale(
+              'en', 'English: Hello it\'s a pleasure to meet you. ');
+      mockFeedback.call(doCmd('nextLine'))
+          .expectSpeechWithLocale('fr', 'français: Comment ca va?');
+      mockFeedback.call(doCmd('nextLine'))
+          .expectSpeechWithLocale('en', 'English: Switching back to English. ');
+      mockFeedback.call(doCmd('nextLine'))
+          .expectSpeechWithLocale('es', 'español: Hola.');
+      mockFeedback.call(doCmd('nextLine'))
+          .expectSpeechWithLocale('en', 'English: Goodbye.');
+      mockFeedback.replay();
     });
 
-TEST_F('ChromeVoxLocaleOutputHelperTest', 'ButtonAndLinkDocTest', function() {
-  const mockFeedback = this.createMockFeedback();
-  this.runWithLoadedTree(this.buttonAndLinkDoc, function(root) {
-    localStorage['languageSwitching'] = 'true';
-    this.setAvailableVoices();
-    mockFeedback
-        .call(doCmd('jumpToTop'))
-        // Use the author-provided language of 'es'.
-        .expectSpeechWithLocale(
-            'es', 'español: This is a paragraph, written in English.')
-        .call(doCmd('nextObject'))
-        .expectSpeechWithLocale('es', 'This is a button, written in English.')
-        .expectSpeechWithLocale(
-            undefined, 'Button', 'Press Search+Space to activate')
-        .call(doCmd('nextObject'))
-        .expectSpeechWithLocale('es', 'Este es un enlace.')
-        .expectSpeechWithLocale(undefined, 'Link');
-    mockFeedback.replay();
-  });
-});
-
+TEST_F(
+    'ChromeVoxLocaleOutputHelperTest', 'ButtonAndLinkDocTest',
+    async function() {
+      const mockFeedback = this.createMockFeedback();
+      const root = await this.runWithLoadedTree(this.buttonAndLinkDoc);
+      localStorage['languageSwitching'] = 'true';
+      this.setAvailableVoices();
+      mockFeedback
+          .call(doCmd('jumpToTop'))
+          // Use the author-provided language of 'es'.
+          .expectSpeechWithLocale(
+              'es', 'español: This is a paragraph, written in English.')
+          .call(doCmd('nextObject'))
+          .expectSpeechWithLocale('es', 'This is a button, written in English.')
+          .expectSpeechWithLocale(
+              undefined, 'Button', 'Press Search+Space to activate')
+          .call(doCmd('nextObject'))
+          .expectSpeechWithLocale('es', 'Este es un enlace.')
+          .expectSpeechWithLocale(undefined, 'Link');
+      mockFeedback.replay();
+    });
 
 TEST_F(
     'ChromeVoxLocaleOutputHelperTest', 'JapaneseAndEnglishUnlabeledDocTest',
-    function() {
+    async function() {
       const mockFeedback = this.createMockFeedback();
-      this.runWithLoadedTree(
-          this.japaneseAndEnglishUnlabeledDoc, function(root) {
-            localStorage['languageSwitching'] = 'true';
-            this.setAvailableVoices();
-            mockFeedback
-                .call(doCmd('jumpToTop'))
-                // Expect the node's contents to be read in one language
-                // (English).
-                // Language detection does not run on small runs of text, like
-                // the one in this test, we are falling back on the UI language
-                // of the browser, which is en-US. Please see testGenPreamble
-                // for more details.
-                .expectSpeechWithLocale(
-                    'en-us',
-                    'Hello, my name is 太田あきひろ. It\'s a pleasure to meet' +
-                        ' you. どうぞよろしくお願いします.');
-            mockFeedback.replay();
-          });
+      const root =
+          await this.runWithLoadedTree(this.japaneseAndEnglishUnlabeledDoc);
+      localStorage['languageSwitching'] = 'true';
+      this.setAvailableVoices();
+      mockFeedback
+          .call(doCmd('jumpToTop'))
+          // Expect the node's contents to be read in one language
+          // (English).
+          // Language detection does not run on small runs of text, like
+          // the one in this test, we are falling back on the UI language
+          // of the browser, which is en-US. Please see testGenPreamble
+          // for more details.
+          .expectSpeechWithLocale(
+              'en-us',
+              'Hello, my name is 太田あきひろ. It\'s a pleasure to meet' +
+                  ' you. どうぞよろしくお願いします.');
+      mockFeedback.replay();
     });
 
-
 TEST_F(
     'ChromeVoxLocaleOutputHelperTest', 'EnglishAndKoreanUnlabeledDocTest',
-    function() {
+    async function() {
       const mockFeedback = this.createMockFeedback();
-      this.runWithLoadedTree(this.englishAndKoreanUnlabeledDoc, function(root) {
-        localStorage['languageSwitching'] = 'true';
-        this.setAvailableVoices();
-        mockFeedback.call(doCmd('jumpToTop'))
-            .expectSpeechWithLocale(
-                'en-us',
-                'This text is written in English. 차에 한하여 중임할 수.' +
-                    ' This text is also written in English.');
-        mockFeedback.replay();
-      });
+      const root =
+          await this.runWithLoadedTree(this.englishAndKoreanUnlabeledDoc);
+      localStorage['languageSwitching'] = 'true';
+      this.setAvailableVoices();
+      mockFeedback.call(doCmd('jumpToTop'))
+          .expectSpeechWithLocale(
+              'en-us',
+              'This text is written in English. 차에 한하여 중임할 수.' +
+                  ' This text is also written in English.');
+      mockFeedback.replay();
     });
 
 TEST_F(
     'ChromeVoxLocaleOutputHelperTest', 'EnglishAndFrenchUnlabeledDocTest',
-    function() {
+    async function() {
       const mockFeedback = this.createMockFeedback();
-      this.runWithLoadedTree(this.englishAndFrenchUnlabeledDoc, function(root) {
-        localStorage['languageSwitching'] = 'true';
-        this.setAvailableVoices();
-        mockFeedback.call(doCmd('jumpToTop'))
-            .expectSpeechWithLocale(
-                'en',
-                'This entire object should be read in English, even' +
-                    ' the following French passage: ' +
-                    'salut mon ami! Ca va? Bien, et toi? It\'s hard to' +
-                    ' differentiate between latin-based languages.');
-        mockFeedback.replay();
-      });
+      const root =
+          await this.runWithLoadedTree(this.englishAndFrenchUnlabeledDoc);
+      localStorage['languageSwitching'] = 'true';
+      this.setAvailableVoices();
+      mockFeedback.call(doCmd('jumpToTop'))
+          .expectSpeechWithLocale(
+              'en',
+              'This entire object should be read in English, even' +
+                  ' the following French passage: ' +
+                  'salut mon ami! Ca va? Bien, et toi? It\'s hard to' +
+                  ' differentiate between latin-based languages.');
+      mockFeedback.replay();
     });
 
 TEST_F(
     'ChromeVoxLocaleOutputHelperTest', 'JapaneseCharacterUnlabeledDocTest',
-    function() {
+    async function() {
       const mockFeedback = this.createMockFeedback();
-      this.runWithLoadedTree(
-          this.japaneseCharacterUnlabeledDoc, function(root) {
-            localStorage['languageSwitching'] = 'true';
-            this.setAvailableVoices();
-            mockFeedback.call(doCmd('jumpToTop'))
-                .expectSpeechWithLocale('en-us', 'ど');
-            mockFeedback.replay();
-          });
+      const root =
+          await this.runWithLoadedTree(this.japaneseCharacterUnlabeledDoc);
+      localStorage['languageSwitching'] = 'true';
+      this.setAvailableVoices();
+      mockFeedback.call(doCmd('jumpToTop'))
+          .expectSpeechWithLocale('en-us', 'ど');
+      mockFeedback.replay();
     });
 
 TEST_F(
     'ChromeVoxLocaleOutputHelperTest', 'JapaneseAndChineseUnlabeledDocTest',
-    function() {
+    async function() {
       const mockFeedback = this.createMockFeedback();
-      this.runWithLoadedTree(this.japaneseAndChineseUnlabeledDoc, function(root) {
-        localStorage['languageSwitching'] = 'true';
-        this.setAvailableVoices();
-        mockFeedback.call(doCmd('jumpToTop'))
-            .expectSpeechWithLocale(
-                'en-us',
-                '天気はいいですね. 右万諭全中結社原済権人点掲年難出面者会追');
-        mockFeedback.replay();
-      });
+      const root =
+          await this.runWithLoadedTree(this.japaneseAndChineseUnlabeledDoc);
+      localStorage['languageSwitching'] = 'true';
+      this.setAvailableVoices();
+      mockFeedback.call(doCmd('jumpToTop'))
+          .expectSpeechWithLocale(
+              'en-us',
+              '天気はいいですね. 右万諭全中結社原済権人点掲年難出面者会追');
+      mockFeedback.replay();
     });
 
 TEST_F(
     'ChromeVoxLocaleOutputHelperTest', 'JapaneseAndChineseLabeledDocTest',
-    function() {
+    async function() {
       const mockFeedback = this.createMockFeedback();
       // Only difference between doc used in this test and
       // this.japaneseAndChineseUnlabeledDoc is the lang="zh" attribute.
-      this.runWithLoadedTree(
-          `
+      const root = await this.runWithLoadedTree(`
         <meta charset="utf-8">
         <p lang="zh">
           天気はいいですね. 右万諭全中結社原済権人点掲年難出面者会追
         </p>
-    `,
-          function(root) {
-            localStorage['languageSwitching'] = 'true';
-            this.setAvailableVoices();
-            mockFeedback.call(doCmd('jumpToTop'))
-                .expectSpeechWithLocale(
-                    'zh',
-                    '中文: 天気はいいですね. 右万諭全中結社原済権人点掲年難出面者会追');
-            mockFeedback.replay();
-          });
+    `);
+      localStorage['languageSwitching'] = 'true';
+      this.setAvailableVoices();
+      mockFeedback.call(doCmd('jumpToTop'))
+          .expectSpeechWithLocale(
+              'zh',
+              '中文: 天気はいいですね. 右万諭全中結社原済権人点掲年難出面者会追');
+      mockFeedback.replay();
     });
 
 TEST_F(
     'ChromeVoxLocaleOutputHelperTest', 'JapaneseAndKoreanUnlabeledDocTest',
-    function() {
+    async function() {
       const mockFeedback = this.createMockFeedback();
-      this.runWithLoadedTree(this.japaneseAndKoreanUnlabeledDoc, function(root) {
-        localStorage['languageSwitching'] = 'true';
-        this.setAvailableVoices();
-        // Language detection runs and assigns language of 'ko' to the node.
-        mockFeedback.call(doCmd('jumpToTop'))
-            .expectSpeechWithLocale(
-                'ko',
-                '한국어: 私は. 법률이 정하는 바에 의하여 대법관이 아닌 법관을 둘 수' +
-                    ' 있다');
-        mockFeedback.replay();
-      });
+      const root =
+          await this.runWithLoadedTree(this.japaneseAndKoreanUnlabeledDoc);
+      localStorage['languageSwitching'] = 'true';
+      this.setAvailableVoices();
+      // Language detection runs and assigns language of 'ko' to the node.
+      mockFeedback.call(doCmd('jumpToTop'))
+          .expectSpeechWithLocale(
+              'ko',
+              '한국어: 私は. 법률이 정하는 바에 의하여 대법관이 아닌 법관을 둘 수' +
+                  ' 있다');
+      mockFeedback.replay();
     });
 
 TEST_F(
     'ChromeVoxLocaleOutputHelperTest', 'AsturianAndJapaneseDocTest',
-    function() {
+    async function() {
       const mockFeedback = this.createMockFeedback();
-      this.runWithLoadedTree(this.asturianAndJapaneseDoc, function(root) {
-        localStorage['languageSwitching'] = 'true';
-        this.setAvailableVoices();
-        mockFeedback.call(doCmd('jumpToTop'))
-            .expectSpeechWithLocale('ja', '日本語: ど')
-            .call(doCmd('nextObject'))
-            .expectSpeechWithLocale(
-                'ast',
-                'asturianu: Pretend that this text is Asturian. Testing' +
-                    ' three-letter language code logic.');
-        mockFeedback.replay();
-      });
+      const root = await this.runWithLoadedTree(this.asturianAndJapaneseDoc);
+      localStorage['languageSwitching'] = 'true';
+      this.setAvailableVoices();
+      mockFeedback.call(doCmd('jumpToTop'))
+          .expectSpeechWithLocale('ja', '日本語: ど')
+          .call(doCmd('nextObject'))
+          .expectSpeechWithLocale(
+              'ast',
+              'asturianu: Pretend that this text is Asturian. Testing' +
+                  ' three-letter language code logic.');
+      mockFeedback.replay();
     });
 
 
 TEST_F(
-    'ChromeVoxLocaleOutputHelperTest', 'LanguageSwitchingOffTest', function() {
+    'ChromeVoxLocaleOutputHelperTest', 'LanguageSwitchingOffTest',
+    async function() {
       const mockFeedback = this.createMockFeedback();
-      this.runWithLoadedTree(this.multipleLanguagesLabeledDoc, function(root) {
-        localStorage['languageSwitching'] = 'false';
-        this.setAvailableVoices();
-        // Locale should not be set if the language switching feature is off.
-        mockFeedback.call(doCmd('jumpToTop'))
-            .expectSpeechWithLocale(undefined, 'Hola.')
-            .call(doCmd('nextObject'))
-            .expectSpeechWithLocale(undefined, 'Hello.')
-            .call(doCmd('nextObject'))
-            .expectSpeechWithLocale(undefined, 'Salut.')
-            .call(doCmd('nextObject'))
-            .expectSpeechWithLocale(undefined, 'Ciao amico.');
-        mockFeedback.replay();
-      });
+      const root =
+          await this.runWithLoadedTree(this.multipleLanguagesLabeledDoc);
+      localStorage['languageSwitching'] = 'false';
+      this.setAvailableVoices();
+      // Locale should not be set if the language switching feature is off.
+      mockFeedback.call(doCmd('jumpToTop'))
+          .expectSpeechWithLocale(undefined, 'Hola.')
+          .call(doCmd('nextObject'))
+          .expectSpeechWithLocale(undefined, 'Hello.')
+          .call(doCmd('nextObject'))
+          .expectSpeechWithLocale(undefined, 'Salut.')
+          .call(doCmd('nextObject'))
+          .expectSpeechWithLocale(undefined, 'Ciao amico.');
+      mockFeedback.replay();
     });
 
-TEST_F('ChromeVoxLocaleOutputHelperTest', 'DefaultToUILocaleTest', function() {
-  const mockFeedback = this.createMockFeedback();
-  this.runWithLoadedTree(
-      this.japaneseAndInvalidLanguagesLabeledDoc, function(root) {
-        localStorage['languageSwitching'] = 'true';
-        this.setAvailableVoices();
-        mockFeedback.call(doCmd('jumpToTop'))
-            .expectSpeechWithLocale('ja', '日本語: どうぞよろしくお願いします')
-            .call(doCmd('nextObject'))
-            .expectSpeechWithLocale('en-us', 'English (United States): Test')
-            .call(doCmd('nextObject'))
-            .expectSpeechWithLocale('en-us', 'Yikes');
-        mockFeedback.replay();
-      });
-});
-
-TEST_F('ChromeVoxLocaleOutputHelperTest', 'NoAvailableVoicesTest', function() {
-  const mockFeedback = this.createMockFeedback();
-  this.runWithLoadedTree(this.vietnameseAndUrduLabeledDoc, function(root) {
-    localStorage['languageSwitching'] = 'true';
-    this.setAvailableVoices();
-    mockFeedback.call(doCmd('jumpToTop'))
-        .expectSpeechWithLocale(
-            'en-us', 'No voice available for language: Vietnamese')
-        .call(doCmd('nextObject'))
-        .expectSpeechWithLocale(
-            'en-us', 'No voice available for language: Urdu');
-    mockFeedback.replay();
-  });
-});
-
-TEST_F('ChromeVoxLocaleOutputHelperTest', 'WordNavigationTest', function() {
-  const mockFeedback = this.createMockFeedback();
-  this.runWithLoadedTree(this.nestedLanguagesLabeledDoc, function() {
-    localStorage['languageSwitching'] = 'true';
-    this.setAvailableVoices();
-    mockFeedback.call(doCmd('jumpToTop'))
-        .expectSpeechWithLocale(
-            'en', 'In the morning, I sometimes eat breakfast.')
-        .call(doCmd('nextLine'))
-        .expectSpeechWithLocale(
-            'fr', 'français: Dans l\'apres-midi, je dejeune.')
-        .call(doCmd('nextWord'))
-        .expectSpeechWithLocale('fr', `l'apres`)
-        .call(doCmd('nextWord'))
-        .expectSpeechWithLocale('fr', `-`)
-        .call(doCmd('nextWord'))
-        .expectSpeechWithLocale('fr', `midi`)
-        .call(doCmd('nextWord'))
-        .expectSpeechWithLocale('fr', `,`)
-        .call(doCmd('nextWord'))
-        .expectSpeechWithLocale('fr', `je`)
-        .call(doCmd('nextWord'))
-        .expectSpeechWithLocale('fr', `dejeune`)
-        .call(doCmd('nextWord'))
-        .expectSpeechWithLocale('fr', `.`)
-        .call(doCmd('nextWord'))
-        .expectSpeechWithLocale('en', `English: Hello`)
-        .call(doCmd('nextWord'))
-        .expectSpeechWithLocale('en', `it's`)
-        .call(doCmd('nextWord'))
-        .expectSpeechWithLocale('en', `a`)
-        .call(doCmd('nextWord'))
-        .expectSpeechWithLocale('en', `pleasure`)
-        .call(doCmd('nextWord'))
-        .expectSpeechWithLocale('en', `to`)
-        .call(doCmd('nextWord'))
-        .expectSpeechWithLocale('en', `meet`)
-        .call(doCmd('nextWord'))
-        .expectSpeechWithLocale('en', `you`)
-        .call(doCmd('nextWord'))
-        .expectSpeechWithLocale('en', `.`)
-        .call(doCmd('nextWord'))
-        .expectSpeechWithLocale('fr', `français: Comment`)
-        .call(doCmd('previousWord'))
-        .expectSpeechWithLocale('en', `English: .`)
-        .call(doCmd('previousWord'))
-        .expectSpeechWithLocale('en', `you`)
-        .replay();
-  });
-});
+TEST_F(
+    'ChromeVoxLocaleOutputHelperTest', 'DefaultToUILocaleTest',
+    async function() {
+      const mockFeedback = this.createMockFeedback();
+      const root = await this.runWithLoadedTree(
+          this.japaneseAndInvalidLanguagesLabeledDoc);
+      localStorage['languageSwitching'] = 'true';
+      this.setAvailableVoices();
+      mockFeedback.call(doCmd('jumpToTop'))
+          .expectSpeechWithLocale('ja', '日本語: どうぞよろしくお願いします')
+          .call(doCmd('nextObject'))
+          .expectSpeechWithLocale('en-us', 'English (United States): Test')
+          .call(doCmd('nextObject'))
+          .expectSpeechWithLocale('en-us', 'Yikes');
+      mockFeedback.replay();
+    });
 
 TEST_F(
-    'ChromeVoxLocaleOutputHelperTest', 'CharacterNavigationTest', function() {
+    'ChromeVoxLocaleOutputHelperTest', 'NoAvailableVoicesTest',
+    async function() {
       const mockFeedback = this.createMockFeedback();
-      this.runWithLoadedTree(this.nestedLanguagesLabeledDoc, function() {
-        localStorage['languageSwitching'] = 'true';
-        this.setAvailableVoices();
-        mockFeedback.call(doCmd('jumpToTop'))
-            .expectSpeechWithLocale(
-                'en', 'In the morning, I sometimes eat breakfast.')
-            .call(doCmd('nextLine'))
-            .expectSpeechWithLocale(
-                'fr', 'français: Dans l\'apres-midi, je dejeune.')
-            .call(doCmd('nextCharacter'))
-            .expectSpeechWithLocale('fr', `a`)
-            .call(doCmd('nextCharacter'))
-            .expectSpeechWithLocale('fr', `n`)
-            .call(doCmd('nextCharacter'))
-            .expectSpeechWithLocale('fr', `s`)
-            .call(doCmd('nextCharacter'))
-            .expectSpeechWithLocale('fr', ` `)
-            .call(doCmd('nextCharacter'))
-            .expectSpeechWithLocale('fr', `l`)
-            .call(doCmd('nextLine'))
-            .expectSpeechWithLocale(
-                'en', `English: Hello it's a pleasure to meet you. `)
-            .call(doCmd('nextCharacter'))
-            .expectSpeechWithLocale('en', `e`)
-            .call(doCmd('previousCharacter'))
-            .expectSpeechWithLocale('en', `H`)
-            .call(doCmd('previousCharacter'))
-            .expectSpeechWithLocale('fr', `français: .`)
-            .call(doCmd('previousCharacter'))
-            .expectSpeechWithLocale('fr', `e`)
-            .call(doCmd('previousCharacter'))
-            .expectSpeechWithLocale('fr', `n`)
-            .call(doCmd('previousCharacter'))
-            .expectSpeechWithLocale('fr', `u`)
-            .call(doCmd('previousCharacter'))
-            .expectSpeechWithLocale('fr', `e`)
-            .call(doCmd('previousCharacter'))
-            .expectSpeechWithLocale('fr', `j`)
-            .replay();
-      });
+      const root =
+          await this.runWithLoadedTree(this.vietnameseAndUrduLabeledDoc);
+      localStorage['languageSwitching'] = 'true';
+      this.setAvailableVoices();
+      mockFeedback.call(doCmd('jumpToTop'))
+          .expectSpeechWithLocale(
+              'en-us', 'No voice available for language: Vietnamese')
+          .call(doCmd('nextObject'))
+          .expectSpeechWithLocale(
+              'en-us', 'No voice available for language: Urdu');
+      mockFeedback.replay();
+    });
+
+TEST_F(
+    'ChromeVoxLocaleOutputHelperTest', 'WordNavigationTest', async function() {
+      const mockFeedback = this.createMockFeedback();
+      await this.runWithLoadedTree(this.nestedLanguagesLabeledDoc);
+      localStorage['languageSwitching'] = 'true';
+      this.setAvailableVoices();
+      mockFeedback.call(doCmd('jumpToTop'))
+          .expectSpeechWithLocale(
+              'en', 'In the morning, I sometimes eat breakfast.')
+          .call(doCmd('nextLine'))
+          .expectSpeechWithLocale(
+              'fr', 'français: Dans l\'apres-midi, je dejeune.')
+          .call(doCmd('nextWord'))
+          .expectSpeechWithLocale('fr', `l'apres`)
+          .call(doCmd('nextWord'))
+          .expectSpeechWithLocale('fr', `-`)
+          .call(doCmd('nextWord'))
+          .expectSpeechWithLocale('fr', `midi`)
+          .call(doCmd('nextWord'))
+          .expectSpeechWithLocale('fr', `,`)
+          .call(doCmd('nextWord'))
+          .expectSpeechWithLocale('fr', `je`)
+          .call(doCmd('nextWord'))
+          .expectSpeechWithLocale('fr', `dejeune`)
+          .call(doCmd('nextWord'))
+          .expectSpeechWithLocale('fr', `.`)
+          .call(doCmd('nextWord'))
+          .expectSpeechWithLocale('en', `English: Hello`)
+          .call(doCmd('nextWord'))
+          .expectSpeechWithLocale('en', `it's`)
+          .call(doCmd('nextWord'))
+          .expectSpeechWithLocale('en', `a`)
+          .call(doCmd('nextWord'))
+          .expectSpeechWithLocale('en', `pleasure`)
+          .call(doCmd('nextWord'))
+          .expectSpeechWithLocale('en', `to`)
+          .call(doCmd('nextWord'))
+          .expectSpeechWithLocale('en', `meet`)
+          .call(doCmd('nextWord'))
+          .expectSpeechWithLocale('en', `you`)
+          .call(doCmd('nextWord'))
+          .expectSpeechWithLocale('en', `.`)
+          .call(doCmd('nextWord'))
+          .expectSpeechWithLocale('fr', `français: Comment`)
+          .call(doCmd('previousWord'))
+          .expectSpeechWithLocale('en', `English: .`)
+          .call(doCmd('previousWord'))
+          .expectSpeechWithLocale('en', `you`)
+          .replay();
+    });
+
+TEST_F(
+    'ChromeVoxLocaleOutputHelperTest', 'CharacterNavigationTest',
+    async function() {
+      const mockFeedback = this.createMockFeedback();
+      await this.runWithLoadedTree(this.nestedLanguagesLabeledDoc);
+      localStorage['languageSwitching'] = 'true';
+      this.setAvailableVoices();
+      mockFeedback.call(doCmd('jumpToTop'))
+          .expectSpeechWithLocale(
+              'en', 'In the morning, I sometimes eat breakfast.')
+          .call(doCmd('nextLine'))
+          .expectSpeechWithLocale(
+              'fr', 'français: Dans l\'apres-midi, je dejeune.')
+          .call(doCmd('nextCharacter'))
+          .expectSpeechWithLocale('fr', `a`)
+          .call(doCmd('nextCharacter'))
+          .expectSpeechWithLocale('fr', `n`)
+          .call(doCmd('nextCharacter'))
+          .expectSpeechWithLocale('fr', `s`)
+          .call(doCmd('nextCharacter'))
+          .expectSpeechWithLocale('fr', ` `)
+          .call(doCmd('nextCharacter'))
+          .expectSpeechWithLocale('fr', `l`)
+          .call(doCmd('nextLine'))
+          .expectSpeechWithLocale(
+              'en', `English: Hello it's a pleasure to meet you. `)
+          .call(doCmd('nextCharacter'))
+          .expectSpeechWithLocale('en', `e`)
+          .call(doCmd('previousCharacter'))
+          .expectSpeechWithLocale('en', `H`)
+          .call(doCmd('previousCharacter'))
+          .expectSpeechWithLocale('fr', `français: .`)
+          .call(doCmd('previousCharacter'))
+          .expectSpeechWithLocale('fr', `e`)
+          .call(doCmd('previousCharacter'))
+          .expectSpeechWithLocale('fr', `n`)
+          .call(doCmd('previousCharacter'))
+          .expectSpeechWithLocale('fr', `u`)
+          .call(doCmd('previousCharacter'))
+          .expectSpeechWithLocale('fr', `e`)
+          .call(doCmd('previousCharacter'))
+          .expectSpeechWithLocale('fr', `j`)
+          .replay();
     });
 
 TEST_F(
     'ChromeVoxLocaleOutputHelperTest', 'SwitchBetweenChineseDialectsTest',
-    function() {
+    async function() {
       const mockFeedback = this.createMockFeedback();
-      this.runWithLoadedTree(this.chineseDoc, function() {
-        localStorage['languageSwitching'] = 'true';
-        this.setAvailableVoices();
-        mockFeedback.call(doCmd('jumpToTop'))
-            .expectSpeechWithLocale('en-us', 'United States')
-            .call(doCmd('nextLine'))
-            .expectSpeechWithLocale(
-                'zh-hans', '中文(简体): Simplified Chinese')
-            .call(doCmd('nextLine'))
-            .expectSpeechWithLocale(
-                'zh-hant', '中文(繁體): Traditional Chinese');
-        mockFeedback.replay();
-      });
+      await this.runWithLoadedTree(this.chineseDoc);
+      localStorage['languageSwitching'] = 'true';
+      this.setAvailableVoices();
+      mockFeedback.call(doCmd('jumpToTop'))
+          .expectSpeechWithLocale('en-us', 'United States')
+          .call(doCmd('nextLine'))
+          .expectSpeechWithLocale('zh-hans', '中文(简体): Simplified Chinese')
+          .call(doCmd('nextLine'))
+          .expectSpeechWithLocale(
+              'zh-hant', '中文(繁體): Traditional Chinese');
+      mockFeedback.replay();
     });
 
 TEST_F(
     'ChromeVoxLocaleOutputHelperTest', 'SwitchBetweenPortugueseDialectsTest',
-    function() {
+    async function() {
       const mockFeedback = this.createMockFeedback();
-      this.runWithLoadedTree(this.portugueseDoc, function() {
-        localStorage['languageSwitching'] = 'true';
-        this.setAvailableVoices();
-        mockFeedback.call(doCmd('jumpToTop'))
-            .expectSpeechWithLocale('en-us', 'United States')
-            .call(doCmd('nextLine'))
-            .expectSpeechWithLocale('pt-br', 'português (Brasil): Brazil')
-            .call(doCmd('nextLine'))
-            .expectSpeechWithLocale('pt-pt', 'português (Portugal): Portugal');
-        mockFeedback.replay();
-      });
+      await this.runWithLoadedTree(this.portugueseDoc);
+      localStorage['languageSwitching'] = 'true';
+      this.setAvailableVoices();
+      mockFeedback.call(doCmd('jumpToTop'))
+          .expectSpeechWithLocale('en-us', 'United States')
+          .call(doCmd('nextLine'))
+          .expectSpeechWithLocale('pt-br', 'português (Brasil): Brazil')
+          .call(doCmd('nextLine'))
+          .expectSpeechWithLocale('pt-pt', 'português (Portugal): Portugal');
+      mockFeedback.replay();
     });
 
 // Tests logic in shouldAnnounceLocale_(). We only announce the locale once when
@@ -581,26 +572,24 @@
 // less specific locales, e.g. 'en-us' -> 'en' should not be announced. Finally,
 // subsequent transitions to the same locale, e.g. 'en' -> 'en-us' should not be
 // announced.
-TEST_F('ChromeVoxLocaleOutputHelperTest', 'MaybeAnnounceLocale', function() {
-  const mockFeedback = this.createMockFeedback();
-  this.runWithLoadedTree(
-      `
+TEST_F(
+    'ChromeVoxLocaleOutputHelperTest', 'MaybeAnnounceLocale', async function() {
+      const mockFeedback = this.createMockFeedback();
+      await this.runWithLoadedTree(`
   <p lang="en">Start</p>
   <p lang="en-ca">Middle</p>
   <p lang="en">Penultimate</p>
   <p lang="en-ca">End</p>
-  `,
-      function() {
-        localStorage['languageSwitching'] = 'true';
-        this.setAvailableVoices();
-        mockFeedback.call(doCmd('jumpToTop'))
-            .expectSpeechWithLocale('en', 'Start')
-            .call(doCmd('nextObject'))
-            .expectSpeechWithLocale('en-ca', 'English (Canada): Middle')
-            .call(doCmd('nextObject'))
-            .expectSpeechWithLocale('en', 'Penultimate')
-            .call(doCmd('nextObject'))
-            .expectSpeechWithLocale('en-ca', 'End')
-            .replay();
-      });
-});
+  `);
+      localStorage['languageSwitching'] = 'true';
+      this.setAvailableVoices();
+      mockFeedback.call(doCmd('jumpToTop'))
+          .expectSpeechWithLocale('en', 'Start')
+          .call(doCmd('nextObject'))
+          .expectSpeechWithLocale('en-ca', 'English (Canada): Middle')
+          .call(doCmd('nextObject'))
+          .expectSpeechWithLocale('en', 'Penultimate')
+          .call(doCmd('nextObject'))
+          .expectSpeechWithLocale('en-ca', 'End')
+          .replay();
+    });
diff --git a/chrome/browser/resources/chromeos/accessibility/chromevox/background/output/output_test.js b/chrome/browser/resources/chromeos/accessibility/chromevox/background/output/output_test.js
index 4acd24f1..ad2414f 100644
--- a/chrome/browser/resources/chromeos/accessibility/chromevox/background/output/output_test.js
+++ b/chrome/browser/resources/chromeos/accessibility/chromevox/background/output/output_test.js
@@ -106,394 +106,371 @@
 };
 
 
-TEST_F('ChromeVoxOutputE2ETest', 'Links', function() {
-  this.runWithLoadedTree('<a href="#">Click here</a>', function(root) {
-    const el = root.firstChild.firstChild;
-    const range = cursors.Range.fromNode(el);
-    const o = new Output().withSpeechAndBraille(range, null, 'navigate');
-    assertEqualsJSON(
-        {
-          string_: 'Click here|Internal link|Press Search+Space to activate',
-          'spans_': [
-            // Attributes.
-            {value: 'name', start: 0, end: 10},
+TEST_F('ChromeVoxOutputE2ETest', 'Links', async function() {
+  const root = await this.runWithLoadedTree('<a href="#">Click here</a>');
+  const el = root.firstChild.firstChild;
+  const range = cursors.Range.fromNode(el);
+  const o = new Output().withSpeechAndBraille(range, null, 'navigate');
+  assertEqualsJSON(
+      {
+        string_: 'Click here|Internal link|Press Search+Space to activate',
+        'spans_': [
+          // Attributes.
+          {value: 'name', start: 0, end: 10},
 
-            // Link earcon (based on the name).
-            {value: {earconId: 'LINK'}, start: 0, end: 10},
+          // Link earcon (based on the name).
+          {value: {earconId: 'LINK'}, start: 0, end: 10},
 
-            {value: {'delay': true}, start: 25, end: 55}
-          ]
-        },
-        o.speechOutputForTest);
-    checkBrailleOutput(
-        'Click here intlnk',
-        [{value: new OutputNodeSpan(el), start: 0, end: 17}], o);
-  });
+          {value: {'delay': true}, start: 25, end: 55}
+        ]
+      },
+      o.speechOutputForTest);
+  checkBrailleOutput(
+      'Click here intlnk', [{value: new OutputNodeSpan(el), start: 0, end: 17}],
+      o);
 });
 
-TEST_F('ChromeVoxOutputE2ETest', 'Checkbox', function() {
-  this.runWithLoadedTree('<input type="checkbox">', function(root) {
-    const el = root.firstChild.firstChild;
-    const range = cursors.Range.fromNode(el);
-    const o = new Output().withSpeechAndBraille(range, null, 'navigate');
-    checkSpeechOutput(
-        '|Check box|Not checked|Press Search+Space to toggle',
-        [
-          {value: new OutputEarconAction('CHECK_OFF'), start: 0, end: 0},
-          {value: 'role', start: 1, end: 10},
-          {value: {'delay': true}, start: 23, end: 51}
-        ],
-        o);
-    checkBrailleOutput(
-        'chk ( )', [{value: new OutputNodeSpan(el), start: 0, end: 7}], o);
-  });
+TEST_F('ChromeVoxOutputE2ETest', 'Checkbox', async function() {
+  const root = await this.runWithLoadedTree('<input type="checkbox">');
+  const el = root.firstChild.firstChild;
+  const range = cursors.Range.fromNode(el);
+  const o = new Output().withSpeechAndBraille(range, null, 'navigate');
+  checkSpeechOutput(
+      '|Check box|Not checked|Press Search+Space to toggle',
+      [
+        {value: new OutputEarconAction('CHECK_OFF'), start: 0, end: 0},
+        {value: 'role', start: 1, end: 10},
+        {value: {'delay': true}, start: 23, end: 51}
+      ],
+      o);
+  checkBrailleOutput(
+      'chk ( )', [{value: new OutputNodeSpan(el), start: 0, end: 7}], o);
 });
 
-TEST_F('ChromeVoxOutputE2ETest', 'InLineTextBoxValueGetsIgnored', function() {
-  this.runWithLoadedTree('<p>OK', function(root) {
-    let el = root.firstChild.firstChild.firstChild;
-    assertEquals('inlineTextBox', el.role);
-    let range = cursors.Range.fromNode(el);
-    let o = new Output().withSpeechAndBraille(range, null, 'navigate');
-    assertEqualsJSON(
-        {
-          string_: 'OK',
-          'spans_': [
-            // Attributes.
-            {value: 'name', start: 0, end: 2}
-          ]
-        },
-        o.speechOutputForTest);
-    checkBrailleOutput(
-        'OK', [{value: new OutputNodeSpan(el), start: 0, end: 2}], o);
+TEST_F(
+    'ChromeVoxOutputE2ETest', 'InLineTextBoxValueGetsIgnored',
+    async function() {
+      const root = await this.runWithLoadedTree('<p>OK');
+      let el = root.firstChild.firstChild.firstChild;
+      assertEquals('inlineTextBox', el.role);
+      let range = cursors.Range.fromNode(el);
+      let o = new Output().withSpeechAndBraille(range, null, 'navigate');
+      assertEqualsJSON(
+          {
+            string_: 'OK',
+            'spans_': [
+              // Attributes.
+              {value: 'name', start: 0, end: 2}
+            ]
+          },
+          o.speechOutputForTest);
+      checkBrailleOutput(
+          'OK', [{value: new OutputNodeSpan(el), start: 0, end: 2}], o);
 
-    el = root.firstChild.firstChild;
-    assertEquals('staticText', el.role);
-    range = cursors.Range.fromNode(el);
-    o = new Output().withSpeechAndBraille(range, null, 'navigate');
-    assertEqualsJSON(
-        {
-          string_: 'OK',
-          'spans_': [
-            // Attributes.
-            {value: 'name', start: 0, end: 2}
-          ]
-        },
-        o.speechOutputForTest);
-    checkBrailleOutput(
-        'OK', [{value: new OutputNodeSpan(el), start: 0, end: 2}], o);
-  });
-});
+      el = root.firstChild.firstChild;
+      assertEquals('staticText', el.role);
+      range = cursors.Range.fromNode(el);
+      o = new Output().withSpeechAndBraille(range, null, 'navigate');
+      assertEqualsJSON(
+          {
+            string_: 'OK',
+            'spans_': [
+              // Attributes.
+              {value: 'name', start: 0, end: 2}
+            ]
+          },
+          o.speechOutputForTest);
+      checkBrailleOutput(
+          'OK', [{value: new OutputNodeSpan(el), start: 0, end: 2}], o);
+    });
 
-TEST_F('ChromeVoxOutputE2ETest', 'Headings', function() {
-  this.runWithLoadedTree(
-      `
+TEST_F('ChromeVoxOutputE2ETest', 'Headings', async function() {
+  const root = await this.runWithLoadedTree(`
       <h1>a</h1><h2>b</h2><h3>c</h3><h4>d</h4><h5>e</h5><h6>f</h6>
-      <h1><a href="a.com">b</a></h1> `,
-      function(root) {
-        let el = root.firstChild;
-        for (let i = 1; i <= 6; ++i) {
-          const range = cursors.Range.fromNode(el);
-          const o = new Output().withSpeechAndBraille(range, null, 'navigate');
-          const letter = String.fromCharCode('a'.charCodeAt(0) + i - 1);
-          assertEqualsJSON(
-              {
-                string_: letter + '|Heading ' + i,
-                'spans_': [
-                  // Attributes.
-                  {value: 'nameOrDescendants', start: 0, end: 1}
-                ]
-              },
-              o.speechOutputForTest);
-          checkBrailleOutput(
-              letter + ' h' + i,
-              [{value: new OutputNodeSpan(el), start: 0, end: 4}], o);
-          el = el.nextSibling;
-        }
+      <h1><a href="a.com">b</a></h1> `);
+  let el = root.firstChild;
+  for (let i = 1; i <= 6; ++i) {
+    const range = cursors.Range.fromNode(el);
+    const o = new Output().withSpeechAndBraille(range, null, 'navigate');
+    const letter = String.fromCharCode('a'.charCodeAt(0) + i - 1);
+    assertEqualsJSON(
+        {
+          string_: letter + '|Heading ' + i,
+          'spans_': [
+            // Attributes.
+            {value: 'nameOrDescendants', start: 0, end: 1}
+          ]
+        },
+        o.speechOutputForTest);
+    checkBrailleOutput(
+        letter + ' h' + i, [{value: new OutputNodeSpan(el), start: 0, end: 4}],
+        o);
+    el = el.nextSibling;
+  }
 
-        range = cursors.Range.fromNode(el);
-        o = new Output().withSpeechAndBraille(range, null, 'navigate');
-        assertEqualsJSON(
-            {
-              string_: 'b|Link|Heading 1',
-              'spans_': [
-                {value: 'name', start: 0, end: 1},
-                {value: new OutputEarconAction('LINK'), start: 0, end: 1},
-                {value: 'role', start: 2, end: 6}
-              ]
-            },
-            o.speechOutputForTest);
-        checkBrailleOutput(
-            'b lnk h1',
-            [
-              {
-                value: new OutputNodeSpan(el.firstChild.firstChild),
-                start: 0,
-                end: 1
-              },
-              {value: new OutputNodeSpan(el), start: 0, end: 8},
-              {value: new OutputNodeSpan(el.firstChild), start: 2, end: 5}
-            ],
-            o);
-      });
+  range = cursors.Range.fromNode(el);
+  o = new Output().withSpeechAndBraille(range, null, 'navigate');
+  assertEqualsJSON(
+      {
+        string_: 'b|Link|Heading 1',
+        'spans_': [
+          {value: 'name', start: 0, end: 1},
+          {value: new OutputEarconAction('LINK'), start: 0, end: 1},
+          {value: 'role', start: 2, end: 6}
+        ]
+      },
+      o.speechOutputForTest);
+  checkBrailleOutput(
+      'b lnk h1',
+      [
+        {value: new OutputNodeSpan(el.firstChild.firstChild), start: 0, end: 1},
+        {value: new OutputNodeSpan(el), start: 0, end: 8},
+        {value: new OutputNodeSpan(el.firstChild), start: 2, end: 5}
+      ],
+      o);
 });
 
 // TODO(crbug.com/901725): test is flaky.
-TEST_F('ChromeVoxOutputE2ETest', 'DISABLED_Audio', function() {
-  this.runWithLoadedTree(
-      '<audio src="foo.mp3" controls></audio>', function(root) {
-        let el = root.find({role: RoleType.BUTTON});
-        let range = cursors.Range.fromNode(el);
-        let o = new Output().withoutHints().withSpeechAndBraille(
-            range, null, 'navigate');
+TEST_F('ChromeVoxOutputE2ETest', 'DISABLED_Audio', async function() {
+  const root =
+      await this.runWithLoadedTree('<audio src="foo.mp3" controls></audio>');
+  let el = root.find({role: RoleType.BUTTON});
+  let range = cursors.Range.fromNode(el);
+  let o =
+      new Output().withoutHints().withSpeechAndBraille(range, null, 'navigate');
 
-        checkSpeechOutput(
-            'play|Disabled|Button|audio|Tool bar',
-            [
-              {value: new OutputEarconAction('BUTTON'), start: 0, end: 4},
-              {value: 'name', start: 21, end: 26},
-              {value: 'role', start: 27, end: 35}
-            ],
-            o);
+  checkSpeechOutput(
+      'play|Disabled|Button|audio|Tool bar',
+      [
+        {value: new OutputEarconAction('BUTTON'), start: 0, end: 4},
+        {value: 'name', start: 21, end: 26}, {value: 'role', start: 27, end: 35}
+      ],
+      o);
 
-        checkBrailleOutput(
-            'play xx btn audio tlbar',
-            [
-              {value: new OutputNodeSpan(el), start: 0, end: 11},
-              {value: new OutputNodeSpan(el.parent), start: 12, end: 23}
-            ],
-            o);
+  checkBrailleOutput(
+      'play xx btn audio tlbar',
+      [
+        {value: new OutputNodeSpan(el), start: 0, end: 11},
+        {value: new OutputNodeSpan(el.parent), start: 12, end: 23}
+      ],
+      o);
 
-        // TODO(dtseng): Replace with a query.
-        el = el.nextSibling.nextSibling.nextSibling;
-        const prevRange = range;
-        range = cursors.Range.fromNode(el);
-        o = new Output().withoutHints().withSpeechAndBraille(
-            range, prevRange, 'navigate');
-        checkSpeechOutput(
-            '|audio time scrubber|Slider|0:00|Min 0|Max 0',
-            [
-              {value: 'name', start: 0, end: 0},
-              {value: new OutputEarconAction('SLIDER'), start: 0, end: 0},
-              {value: 'description', start: 1, end: 20},
-              {value: 'role', start: 21, end: 27},
-              {value: 'value', start: 28, end: 32}
-            ],
-            o);
-        checkBrailleOutput(
-            'audio time scrubber sldr 0:00 min:0 max:0',
-            [{value: new OutputNodeSpan(el), start: 0, end: 41}], o);
-      });
+  // TODO(dtseng): Replace with a query.
+  el = el.nextSibling.nextSibling.nextSibling;
+  const prevRange = range;
+  range = cursors.Range.fromNode(el);
+  o = new Output().withoutHints().withSpeechAndBraille(
+      range, prevRange, 'navigate');
+  checkSpeechOutput(
+      '|audio time scrubber|Slider|0:00|Min 0|Max 0',
+      [
+        {value: 'name', start: 0, end: 0},
+        {value: new OutputEarconAction('SLIDER'), start: 0, end: 0},
+        {value: 'description', start: 1, end: 20},
+        {value: 'role', start: 21, end: 27},
+        {value: 'value', start: 28, end: 32}
+      ],
+      o);
+  checkBrailleOutput(
+      'audio time scrubber sldr 0:00 min:0 max:0',
+      [{value: new OutputNodeSpan(el), start: 0, end: 41}], o);
 });
 
-TEST_F('ChromeVoxOutputE2ETest', 'Input', function() {
-  this.runWithLoadedTree(
+TEST_F('ChromeVoxOutputE2ETest', 'Input', async function() {
+  const root = await this.runWithLoadedTree(
       '<input type="text"></input>' +
-          '<input type="email"></input>' +
-          '<input type="password"></input>' +
-          '<input type="tel"></input>' +
-          '<input type="number"></input>' +
-          '<input type="time"></input>' +
-          '<input type="date"></input>' +
-          '<input type="file"</input>' +
-          '<input type="search"</input>' +
-          '<input type="invalidType"</input>',
-      function(root) {
-        const expectedSpansNonSearchBox = [
-          {value: 'name', start: 0, end: 0},
-          {value: new OutputEarconAction('EDITABLE_TEXT'), start: 0, end: 0},
-          {value: new OutputSelectionSpan(0, 0, 0), start: 1, end: 1},
-          {value: 'value', start: 1, end: 1}, {value: 'inputType', start: 2}
-        ];
-        const expectedSpansForSearchBox = [
-          {value: 'name', start: 0, end: 0},
-          {value: new OutputEarconAction('EDITABLE_TEXT'), start: 0, end: 0},
-          {value: new OutputSelectionSpan(0, 0, 0), start: 1, end: 1},
-          {value: 'value', start: 1, end: 1}, {value: 'role', start: 2, end: 8}
-        ];
+      '<input type="email"></input>' +
+      '<input type="password"></input>' +
+      '<input type="tel"></input>' +
+      '<input type="number"></input>' +
+      '<input type="time"></input>' +
+      '<input type="date"></input>' +
+      '<input type="file"</input>' +
+      '<input type="search"</input>' +
+      '<input type="invalidType"</input>');
+  const expectedSpansNonSearchBox = [
+    {value: 'name', start: 0, end: 0},
+    {value: new OutputEarconAction('EDITABLE_TEXT'), start: 0, end: 0},
+    {value: new OutputSelectionSpan(0, 0, 0), start: 1, end: 1},
+    {value: 'value', start: 1, end: 1}, {value: 'inputType', start: 2}
+  ];
+  const expectedSpansForSearchBox = [
+    {value: 'name', start: 0, end: 0},
+    {value: new OutputEarconAction('EDITABLE_TEXT'), start: 0, end: 0},
+    {value: new OutputSelectionSpan(0, 0, 0), start: 1, end: 1},
+    {value: 'value', start: 1, end: 1}, {value: 'role', start: 2, end: 8}
+  ];
 
-        const expectedSpeechValues = [
-          '||Edit text', '||Edit text, email entry', '||Password edit text',
-          '||Edit text numeric only',
+  const expectedSpeechValues = [
+    '||Edit text', '||Edit text, email entry', '||Password edit text',
+    '||Edit text numeric only',
+    [
+      '|Spin button',
+      [
+        {value: 'name', start: 0, end: 0},
+        {value: new OutputEarconAction('LISTBOX'), start: 0, end: 0},
+        {value: 'role', start: 1, end: 12}
+      ]
+    ],
+    ['Time control', [{value: 'role', start: 0, end: 12}]],
+    ['Date control', [{value: 'role', start: 0, end: 12}]],
+    [
+      'No file chosen, Choose File|Button',
+      [
+        {value: 'name', start: 0, end: 27},
+        {value: new OutputEarconAction('BUTTON'), start: 0, end: 27},
+        {value: 'role', start: 28, end: 34}
+      ]
+    ],
+    '||Search', '||Edit text'
+  ];
+  // TODO(plundblad): Some of these are wrong, there should be an initial
+  // space for the cursor in edit fields.
+  const expectedBrailleValues = [
+    ' ed', ' @ed 8dot', ' pwded', ' #ed', {string_: 'spnbtn', spans_: []},
+    {string_: 'time'}, {string_: 'date'},
+    {string_: 'No file chosen, Choose File btn'}, ' search', ' ed'
+  ];
+  assertEquals(expectedSpeechValues.length, expectedBrailleValues.length);
+
+  let el = root.firstChild.firstChild;
+  expectedSpeechValues.forEach(function(expectedValue) {
+    const range = cursors.Range.fromNode(el);
+    const o = new Output().withoutHints().withSpeechAndBraille(
+        range, null, 'navigate');
+    let expectedSpansForValue = null;
+    if (typeof expectedValue === 'object') {
+      checkSpeechOutput(expectedValue[0], expectedValue[1], o);
+    } else {
+      expectedSpansForValue = expectedValue === '||Search' ?
+          expectedSpansForSearchBox :
+          expectedSpansNonSearchBox;
+      expectedSpansForValue[4].end = expectedValue.length;
+      checkSpeechOutput(expectedValue, expectedSpansForValue, o);
+    }
+    el = el.nextSibling;
+  });
+
+  el = root.firstChild.firstChild;
+  expectedBrailleValues.forEach(function(expectedValue) {
+    const range = cursors.Range.fromNode(el);
+    const o = new Output().withoutHints().withBraille(range, null, 'navigate');
+    if (typeof expectedValue === 'string') {
+      checkBrailleOutput(
+          expectedValue,
           [
-            '|Spin button',
-            [
-              {value: 'name', start: 0, end: 0},
-              {value: new OutputEarconAction('LISTBOX'), start: 0, end: 0},
-              {value: 'role', start: 1, end: 12}
-            ]
+            {value: {startIndex: 0, endIndex: 0}, start: 0, end: 0},
+            {value: new OutputNodeSpan(el), start: 0, end: expectedValue.length}
           ],
-          ['Time control', [{value: 'role', start: 0, end: 12}]],
-          ['Date control', [{value: 'role', start: 0, end: 12}]],
-          [
-            'No file chosen, Choose File|Button',
-            [
-              {value: 'name', start: 0, end: 27},
-              {value: new OutputEarconAction('BUTTON'), start: 0, end: 27},
-              {value: 'role', start: 28, end: 34}
-            ]
-          ],
-          '||Search', '||Edit text'
-        ];
-        // TODO(plundblad): Some of these are wrong, there should be an initial
-        // space for the cursor in edit fields.
-        const expectedBrailleValues = [
-          ' ed', ' @ed 8dot', ' pwded', ' #ed', {string_: 'spnbtn', spans_: []},
-          {string_: 'time'}, {string_: 'date'},
-          {string_: 'No file chosen, Choose File btn'}, ' search', ' ed'
-        ];
-        assertEquals(expectedSpeechValues.length, expectedBrailleValues.length);
+          o);
+    } else {
+      let spans = [{
+        value: new OutputNodeSpan(el),
+        start: 0,
+        end: expectedValue.string_.length
+      }];
+      if (expectedValue.spans_) {
+        spans = spans.concat(expectedValue.spans_);
+      }
 
-        let el = root.firstChild.firstChild;
-        expectedSpeechValues.forEach(function(expectedValue) {
-          const range = cursors.Range.fromNode(el);
-          const o = new Output().withoutHints().withSpeechAndBraille(
-              range, null, 'navigate');
-          let expectedSpansForValue = null;
-          if (typeof expectedValue === 'object') {
-            checkSpeechOutput(expectedValue[0], expectedValue[1], o);
-          } else {
-            expectedSpansForValue = expectedValue === '||Search' ?
-                expectedSpansForSearchBox :
-                expectedSpansNonSearchBox;
-            expectedSpansForValue[4].end = expectedValue.length;
-            checkSpeechOutput(expectedValue, expectedSpansForValue, o);
-          }
-          el = el.nextSibling;
-        });
-
-        el = root.firstChild.firstChild;
-        expectedBrailleValues.forEach(function(expectedValue) {
-          const range = cursors.Range.fromNode(el);
-          const o =
-              new Output().withoutHints().withBraille(range, null, 'navigate');
-          if (typeof expectedValue === 'string') {
-            checkBrailleOutput(
-                expectedValue,
-                [
-                  {value: {startIndex: 0, endIndex: 0}, start: 0, end: 0}, {
-                    value: new OutputNodeSpan(el),
-                    start: 0,
-                    end: expectedValue.length
-                  }
-                ],
-                o);
-          } else {
-            let spans = [{
-              value: new OutputNodeSpan(el),
-              start: 0,
-              end: expectedValue.string_.length
-            }];
-            if (expectedValue.spans_) {
-              spans = spans.concat(expectedValue.spans_);
-            }
-
-            checkBrailleOutput(expectedValue.string_, spans, o);
-          }
-          el = el.nextSibling;
-        });
-      });
+      checkBrailleOutput(expectedValue.string_, spans, o);
+    }
+    el = el.nextSibling;
+  });
 });
 
-TEST_F('ChromeVoxOutputE2ETest', 'List', function() {
-  this.runWithLoadedTree(
-      '<ul aria-label="first"><li aria-label="a">a<li>b<li>c</ul>',
-      function(root) {
-        const el = root.firstChild.firstChild;
-        const range = cursors.Range.fromNode(el);
-        const o = new Output().withSpeechAndBraille(range, null, 'navigate');
-        checkSpeechOutput(
-            'a|List item|first|List|with 3 items',
-            [
-              {value: {earconId: 'LIST_ITEM'}, start: 0, end: 1},
-              {value: 'name', start: 12, end: 17},
-              {value: 'role', start: 18, end: 22}
-            ],
-            o);
-        // TODO(plundblad): This output is wrong.  Add special handling for
-        // braille here.
-        checkBrailleOutput(
-            'a lstitm first lst +3',
-            [
-              {value: new OutputNodeSpan(el), start: 0, end: 8},
-              {value: new OutputNodeSpan(el.parent), start: 9, end: 21}
-            ],
-            o);
-      });
+TEST_F('ChromeVoxOutputE2ETest', 'List', async function() {
+  const root = await this.runWithLoadedTree(
+      '<ul aria-label="first"><li aria-label="a">a<li>b<li>c</ul>');
+  const el = root.firstChild.firstChild;
+  const range = cursors.Range.fromNode(el);
+  const o = new Output().withSpeechAndBraille(range, null, 'navigate');
+  checkSpeechOutput(
+      'a|List item|first|List|with 3 items',
+      [
+        {value: {earconId: 'LIST_ITEM'}, start: 0, end: 1},
+        {value: 'name', start: 12, end: 17}, {value: 'role', start: 18, end: 22}
+      ],
+      o);
+  // TODO(plundblad): This output is wrong.  Add special handling for
+  // braille here.
+  checkBrailleOutput(
+      'a lstitm first lst +3',
+      [
+        {value: new OutputNodeSpan(el), start: 0, end: 8},
+        {value: new OutputNodeSpan(el.parent), start: 9, end: 21}
+      ],
+      o);
 });
 
-TEST_F('ChromeVoxOutputE2ETest', 'Tree', function() {
-  this.runWithLoadedTree(
-      `
+TEST_F('ChromeVoxOutputE2ETest', 'Tree', async function() {
+  const root = await this.runWithLoadedTree(`
     <ul role="tree" style="list-style-type:none">
       <li aria-expanded="true" role="treeitem">a
       <li role="treeitem">b
       <li aria-expanded="false" role="treeitem">c
     </ul>
-  `,
-      function(root) {
-        let el = root.firstChild.children[0].firstChild;
-        let range = cursors.Range.fromNode(el);
-        let o = new Output().withSpeechAndBraille(range, null, 'navigate');
-        checkSpeechOutput(
-            'a|Tree item|Expanded| 1 of 3 | level 1 |Tree|with 3 items',
-            [
-              {value: 'name', 'start': 0, end: 1},
-              {value: 'state', start: 12, end: 20},
-              {value: 'role', 'start': 40, end: 44},
-            ],
-            o);
-        checkBrailleOutput(
-            'a tritm - 1/3 level 1 tree +3',
-            [
-              {value: new OutputNodeSpan(el), start: 0, end: 1},
-              {value: new OutputNodeSpan(el.parent), start: 2, end: 22},
-              {value: new OutputNodeSpan(el.parent.parent), start: 22, end: 29}
-            ],
-            o);
+  `);
+  let el = root.firstChild.children[0].firstChild;
+  let range = cursors.Range.fromNode(el);
+  let o = new Output().withSpeechAndBraille(range, null, 'navigate');
+  checkSpeechOutput(
+      'a|Tree item|Expanded| 1 of 3 | level 1 |Tree|with 3 items',
+      [
+        {value: 'name', 'start': 0, end: 1},
+        {value: 'state', start: 12, end: 20},
+        {value: 'role', 'start': 40, end: 44},
+      ],
+      o);
+  checkBrailleOutput(
+      'a tritm - 1/3 level 1 tree +3',
+      [
+        {value: new OutputNodeSpan(el), start: 0, end: 1},
+        {value: new OutputNodeSpan(el.parent), start: 2, end: 22},
+        {value: new OutputNodeSpan(el.parent.parent), start: 22, end: 29}
+      ],
+      o);
 
-        el = root.firstChild.children[1].firstChild;
-        range = cursors.Range.fromNode(el);
-        o = new Output().withSpeechAndBraille(range, null, 'navigate');
-        checkSpeechOutput(
-            'b|Tree item| 2 of 3 | level 1 |Tree|with 3 items',
-            [
-              {value: 'name', start: 0, end: 1},
-              {value: 'role', 'start': 31, end: 35}
-            ],
-            o);
-        checkBrailleOutput(
-            'b tritm 2/3 level 1 tree +3',
-            [
-              {value: new OutputNodeSpan(el), start: 0, end: 1},
-              {value: new OutputNodeSpan(el.parent), start: 2, end: 20},
-              {value: new OutputNodeSpan(el.parent.parent), start: 20, end: 27}
-            ],
-            o);
+  el = root.firstChild.children[1].firstChild;
+  range = cursors.Range.fromNode(el);
+  o = new Output().withSpeechAndBraille(range, null, 'navigate');
+  checkSpeechOutput(
+      'b|Tree item| 2 of 3 | level 1 |Tree|with 3 items',
+      [
+        {value: 'name', start: 0, end: 1}, {value: 'role', 'start': 31, end: 35}
+      ],
+      o);
+  checkBrailleOutput(
+      'b tritm 2/3 level 1 tree +3',
+      [
+        {value: new OutputNodeSpan(el), start: 0, end: 1},
+        {value: new OutputNodeSpan(el.parent), start: 2, end: 20},
+        {value: new OutputNodeSpan(el.parent.parent), start: 20, end: 27}
+      ],
+      o);
 
-        el = root.firstChild.children[2].firstChild;
-        range = cursors.Range.fromNode(el);
-        o = new Output().withSpeechAndBraille(range, null, 'navigate');
-        checkSpeechOutput(
-            'c|Tree item|Collapsed| 3 of 3 | level 1 |Tree|with 3 items',
-            [
-              {value: 'name', 'start': 0, end: 1},
-              {value: 'state', start: 12, end: 21},
-              {value: 'role', 'start': 41, end: 45},
-            ],
-            o);
-        checkBrailleOutput(
-            'c tritm + 3/3 level 1 tree +3',
-            [
-              {value: new OutputNodeSpan(el), start: 0, end: 1},
-              {value: new OutputNodeSpan(el.parent), start: 2, end: 22},
-              {value: new OutputNodeSpan(el.parent.parent), start: 22, end: 29}
-            ],
-            o);
-      });
+  el = root.firstChild.children[2].firstChild;
+  range = cursors.Range.fromNode(el);
+  o = new Output().withSpeechAndBraille(range, null, 'navigate');
+  checkSpeechOutput(
+      'c|Tree item|Collapsed| 3 of 3 | level 1 |Tree|with 3 items',
+      [
+        {value: 'name', 'start': 0, end: 1},
+        {value: 'state', start: 12, end: 21},
+        {value: 'role', 'start': 41, end: 45},
+      ],
+      o);
+  checkBrailleOutput(
+      'c tritm + 3/3 level 1 tree +3',
+      [
+        {value: new OutputNodeSpan(el), start: 0, end: 1},
+        {value: new OutputNodeSpan(el.parent), start: 2, end: 22},
+        {value: new OutputNodeSpan(el.parent.parent), start: 22, end: 29}
+      ],
+      o);
 });
 
-TEST_F('ChromeVoxOutputE2ETest', 'Menu', function() {
+TEST_F('ChromeVoxOutputE2ETest', 'Menu', async function() {
   const site = `
     <div role="menu">
       <div role="menuitem">a</div>
@@ -502,78 +479,72 @@
     </div>
     <div role="menubar" aria-orientation="horizontal"></div>
   `;
-  this.runWithLoadedTree(site, function(root) {
-    let el = root.firstChild.firstChild;
-    let range = cursors.Range.fromNode(el);
-    let o = new Output().withSpeechAndBraille(range, null, 'navigate');
-    checkSpeechOutput(
-        'a|Menu item| 1 of 3 |Menu',
-        [
-          {value: 'name', start: 0, end: 1}, {value: 'role', start: 21, end: 25}
-        ],
-        o);
-    checkBrailleOutput(
-        'a mnuitm 1/3 mnu',
-        [
-          {value: new OutputNodeSpan(el), start: 0, end: 12},
-          {value: new OutputNodeSpan(el.parent), start: 13, end: 16}
-        ],
-        o);
+  const root = await this.runWithLoadedTree(site);
+  let el = root.firstChild.firstChild;
+  let range = cursors.Range.fromNode(el);
+  let o = new Output().withSpeechAndBraille(range, null, 'navigate');
+  checkSpeechOutput(
+      'a|Menu item| 1 of 3 |Menu',
+      [{value: 'name', start: 0, end: 1}, {value: 'role', start: 21, end: 25}],
+      o);
+  checkBrailleOutput(
+      'a mnuitm 1/3 mnu',
+      [
+        {value: new OutputNodeSpan(el), start: 0, end: 12},
+        {value: new OutputNodeSpan(el.parent), start: 13, end: 16}
+      ],
+      o);
 
-    // Ancestry.
-    el = root.firstChild;
-    range = cursors.Range.fromNode(el);
-    o = new Output().withSpeechAndBraille(range, null, 'navigate');
-    checkSpeechOutput(
-        'Menu|with 3 items|' +
-            'Press up or down arrow to navigate; enter to activate',
-        [
-          {value: 'role', start: 0, end: 4},
-          {value: {delay: true}, start: 18, end: 71}
-        ],
-        o);
+  // Ancestry.
+  el = root.firstChild;
+  range = cursors.Range.fromNode(el);
+  o = new Output().withSpeechAndBraille(range, null, 'navigate');
+  checkSpeechOutput(
+      'Menu|with 3 items|' +
+          'Press up or down arrow to navigate; enter to activate',
+      [
+        {value: 'role', start: 0, end: 4},
+        {value: {delay: true}, start: 18, end: 71}
+      ],
+      o);
 
-    el = root.lastChild;
-    range = cursors.Range.fromNode(el);
-    o = new Output().withSpeechAndBraille(range, null, 'navigate');
-    checkSpeechOutput(
-        'Menu bar|Press left or right arrow to navigate; enter to activate',
-        [
-          {value: 'role', start: 0, end: 8},
-          {value: {delay: true}, start: 9, end: 65}
-        ],
-        o);
-  });
+  el = root.lastChild;
+  range = cursors.Range.fromNode(el);
+  o = new Output().withSpeechAndBraille(range, null, 'navigate');
+  checkSpeechOutput(
+      'Menu bar|Press left or right arrow to navigate; enter to activate',
+      [
+        {value: 'role', start: 0, end: 8},
+        {value: {delay: true}, start: 9, end: 65}
+      ],
+      o);
 });
 
-TEST_F('ChromeVoxOutputE2ETest', 'ListBox', function() {
-  this.runWithLoadedTree(
-      `
+TEST_F('ChromeVoxOutputE2ETest', 'ListBox', async function() {
+  const root = await this.runWithLoadedTree(`
     <select multiple>
       <option>1</option>
       <option>2</option>
     </select>
-  `,
-      function(root) {
-        const el = root.firstChild.firstChild.firstChild;
-        const range = cursors.Range.fromNode(el);
-        const o = new Output().withSpeechAndBraille(range, null, 'navigate');
-        checkSpeechOutput(
-            '1|List item| 1 of 2 |Not selected|List box|with 2 items',
-            [
-              {value: 'name', start: 0, end: 1},
-              {value: new OutputEarconAction('LIST_ITEM'), start: 0, end: 1},
-              {value: 'role', start: 34, end: 42}
-            ],
-            o);
-        checkBrailleOutput(
-            '1 lstitm 1/2 ( ) lstbx +2',
-            [
-              {value: new OutputNodeSpan(el), start: 0, end: 16},
-              {value: new OutputNodeSpan(el.parent), start: 17, end: 25}
-            ],
-            o);
-      });
+  `);
+  const el = root.firstChild.firstChild.firstChild;
+  const range = cursors.Range.fromNode(el);
+  const o = new Output().withSpeechAndBraille(range, null, 'navigate');
+  checkSpeechOutput(
+      '1|List item| 1 of 2 |Not selected|List box|with 2 items',
+      [
+        {value: 'name', start: 0, end: 1},
+        {value: new OutputEarconAction('LIST_ITEM'), start: 0, end: 1},
+        {value: 'role', start: 34, end: 42}
+      ],
+      o);
+  checkBrailleOutput(
+      '1 lstitm 1/2 ( ) lstbx +2',
+      [
+        {value: new OutputNodeSpan(el), start: 0, end: 16},
+        {value: new OutputNodeSpan(el.parent), start: 17, end: 25}
+      ],
+      o);
 });
 
 SYNC_TEST_F('ChromeVoxOutputE2ETest', 'MessageIdAndEarconValidity', function() {
@@ -664,249 +635,208 @@
   }
 });
 
-TEST_F('ChromeVoxOutputE2ETest', 'DivOmitsRole', function() {
-  this.runWithLoadedTree(
-      `
+TEST_F('ChromeVoxOutputE2ETest', 'DivOmitsRole', async function() {
+  const root = await this.runWithLoadedTree(`
     <div>that has content</div>
     <div></div>
     <div role='group'><div>nested content</div></div>
-  `,
-      function(root) {
-        const el = root.firstChild.firstChild;
-        const range = cursors.Range.fromNode(el);
-        const o = new Output().withSpeechAndBraille(range, null, 'navigate');
-        checkSpeechOutput(
-            'that has content', [{value: 'name', start: 0, end: 16}], o);
-        checkBrailleOutput(
-            'that has content',
-            [{value: new OutputNodeSpan(el), start: 0, end: 16}], o);
-      });
+  `);
+  const el = root.firstChild.firstChild;
+  const range = cursors.Range.fromNode(el);
+  const o = new Output().withSpeechAndBraille(range, null, 'navigate');
+  checkSpeechOutput(
+      'that has content', [{value: 'name', start: 0, end: 16}], o);
+  checkBrailleOutput(
+      'that has content', [{value: new OutputNodeSpan(el), start: 0, end: 16}],
+      o);
 });
 
-TEST_F('ChromeVoxOutputE2ETest', 'LessVerboseAncestry', function() {
-  this.runWithLoadedTree(
-      `
+TEST_F('ChromeVoxOutputE2ETest', 'LessVerboseAncestry', async function() {
+  const root = await this.runWithLoadedTree(`
     <div role="banner"><p>inside</p></div>
     <div role="banner"><p>inside</p></div>
     <div role="navigation"><p>inside</p></div>
-  `,
-      function(root) {
-        const first = root.children[0].firstChild;
-        const second = root.children[1].firstChild;
-        const third = root.children[2].firstChild;
-        const firstRange = cursors.Range.fromNode(first);
-        const secondRange = cursors.Range.fromNode(second);
-        const thirdRange = cursors.Range.fromNode(third);
+  `);
+  const first = root.children[0].firstChild;
+  const second = root.children[1].firstChild;
+  const third = root.children[2].firstChild;
+  const firstRange = cursors.Range.fromNode(first);
+  const secondRange = cursors.Range.fromNode(second);
+  const thirdRange = cursors.Range.fromNode(third);
 
-        const oWithoutPrev =
-            new Output().withSpeech(firstRange, null, 'navigate');
-        const oWithPrev =
-            new Output().withSpeech(secondRange, firstRange, 'navigate');
-        const oWithPrevExit =
-            new Output().withSpeech(thirdRange, secondRange, 'navigate');
-        assertEquals('inside|Banner', oWithoutPrev.speechOutputForTest.string_);
+  const oWithoutPrev = new Output().withSpeech(firstRange, null, 'navigate');
+  const oWithPrev =
+      new Output().withSpeech(secondRange, firstRange, 'navigate');
+  const oWithPrevExit =
+      new Output().withSpeech(thirdRange, secondRange, 'navigate');
+  assertEquals('inside|Banner', oWithoutPrev.speechOutputForTest.string_);
 
-        // Make sure we don't read the exited ancestry change.
-        assertEquals('inside|Banner', oWithPrev.speechOutputForTest.string_);
+  // Make sure we don't read the exited ancestry change.
+  assertEquals('inside|Banner', oWithPrev.speechOutputForTest.string_);
 
-        // Different role; do read the exited ancestry here.
-        assertEquals(
-            'inside|Navigation', oWithPrevExit.speechOutputForTest.string_);
-      });
+  // Different role; do read the exited ancestry here.
+  assertEquals('inside|Navigation', oWithPrevExit.speechOutputForTest.string_);
 });
 
-TEST_F('ChromeVoxOutputE2ETest', 'Brief', function() {
-  this.runWithLoadedTree(
-      `
+TEST_F('ChromeVoxOutputE2ETest', 'Brief', async function() {
+  const root = await this.runWithLoadedTree(`
     <div role="article"><p>inside</p></div>
-  `,
-      function(root) {
-        const node = root.children[0].firstChild;
-        const range = cursors.Range.fromNode(node);
+  `);
+  const node = root.children[0].firstChild;
+  const range = cursors.Range.fromNode(node);
 
-        localStorage['useVerboseMode'] = 'false';
-        const oWithoutPrev = new Output().withSpeech(range, null, 'navigate');
-        assertEquals('inside', oWithoutPrev.speechOutputForTest.string_);
-      });
+  localStorage['useVerboseMode'] = 'false';
+  const oWithoutPrev = new Output().withSpeech(range, null, 'navigate');
+  assertEquals('inside', oWithoutPrev.speechOutputForTest.string_);
 });
 
-TEST_F('ChromeVoxOutputE2ETest', 'AuralStyledHeadings', function() {
+TEST_F('ChromeVoxOutputE2ETest', 'AuralStyledHeadings', async function() {
   function toFixed(num) {
     return parseFloat(Number(num).toFixed(1));
   }
-  this.runWithLoadedTree(
-      `
+  const root = await this.runWithLoadedTree(`
       <h1>a</h1><h2>b</h2><h3>c</h3><h4>d</h4><h5>e</h5><h6>f</h6>
-      <h1><a href="a.com">b</a></h1> `,
-      function(root) {
-        let el = root.firstChild;
-        for (let i = 1; i <= 6; ++i) {
-          const range = cursors.Range.fromNode(el);
-          const o = new Output().withRichSpeech(range, null, 'navigate');
-          const letter = String.fromCharCode('a'.charCodeAt(0) + i - 1);
-          assertEqualsJSON(
-              {
-                string_: letter + '|Heading ' + i,
-                'spans_': [
-                  // Aural styles.
-                  {
-                    value: {'relativePitch': toFixed(-0.1 * i)},
-                    start: 0,
-                    end: 0
-                  },
+      <h1><a href="a.com">b</a></h1> `);
+  let el = root.firstChild;
+  for (let i = 1; i <= 6; ++i) {
+    const range = cursors.Range.fromNode(el);
+    const o = new Output().withRichSpeech(range, null, 'navigate');
+    const letter = String.fromCharCode('a'.charCodeAt(0) + i - 1);
+    assertEqualsJSON(
+        {
+          string_: letter + '|Heading ' + i,
+          'spans_': [
+            // Aural styles.
+            {value: {'relativePitch': toFixed(-0.1 * i)}, start: 0, end: 0},
 
-                  // Attributes.
-                  {value: 'nameOrDescendants', start: 0, end: 1},
+            // Attributes.
+            {value: 'nameOrDescendants', start: 0, end: 1},
 
-                  {value: {'relativePitch': -0.2}, start: 2, end: 2}
-                ]
-              },
-              o.speechOutputForTest);
-          el = el.nextSibling;
-        }
-      });
+            {value: {'relativePitch': -0.2}, start: 2, end: 2}
+          ]
+        },
+        o.speechOutputForTest);
+    el = el.nextSibling;
+  }
 });
 
-TEST_F('ChromeVoxOutputE2ETest', 'ToggleButton', function() {
-  this.runWithLoadedTree(
-      `
-      <div role="button" aria-pressed="true">Subscribe</div>`,
-      function(root) {
-        const el = root.firstChild;
-        const o = new Output().withSpeechAndBraille(cursors.Range.fromNode(el));
-        assertEqualsJSON(
-            {
-              string_:
-                  '|Subscribe|Toggle Button|Pressed|Press Search+Space to toggle',
-              spans_: [
-                {value: {earconId: 'CHECK_ON'}, start: 0, end: 0},
-                {value: 'name', start: 1, end: 10},
-                {value: 'role', start: 11, end: 24},
-                {value: {'delay': true}, start: 33, end: 61}
-              ]
-            },
-            o.speechOutputForTest);
-        assertEquals('Subscribe tgl btn =', o.brailleOutputForTest.string_);
-      });
+TEST_F('ChromeVoxOutputE2ETest', 'ToggleButton', async function() {
+  const root = await this.runWithLoadedTree(`
+      <div role="button" aria-pressed="true">Subscribe</div>`);
+  const el = root.firstChild;
+  const o = new Output().withSpeechAndBraille(cursors.Range.fromNode(el));
+  assertEqualsJSON(
+      {
+        string_:
+            '|Subscribe|Toggle Button|Pressed|Press Search+Space to toggle',
+        spans_: [
+          {value: {earconId: 'CHECK_ON'}, start: 0, end: 0},
+          {value: 'name', start: 1, end: 10},
+          {value: 'role', start: 11, end: 24},
+          {value: {'delay': true}, start: 33, end: 61}
+        ]
+      },
+      o.speechOutputForTest);
+  assertEquals('Subscribe tgl btn =', o.brailleOutputForTest.string_);
 });
 
-TEST_F('ChromeVoxOutputE2ETest', 'JoinDescendants', function() {
-  this.runWithLoadedTree(
-      `
+TEST_F('ChromeVoxOutputE2ETest', 'JoinDescendants', async function() {
+  const root = await this.runWithLoadedTree(`
       <p>This</p>
       <p>fragment</p>
       <p>Should be separated</p>
       <p>with spaces</p>
-    `,
-      function(root) {
-        const unjoined = new Output().format('$descendants', root);
-        assertEquals(
-            'This|fragment|Should be separated|with spaces',
-            unjoined.speechOutputForTest.string_);
+    `);
+  const unjoined = new Output().format('$descendants', root);
+  assertEquals(
+      'This|fragment|Should be separated|with spaces',
+      unjoined.speechOutputForTest.string_);
 
-        const joined = new Output().format('$joinedDescendants', root);
-        assertEquals(
-            'This fragment Should be separated with spaces',
-            joined.speechOutputForTest.string_);
-      });
+  const joined = new Output().format('$joinedDescendants', root);
+  assertEquals(
+      'This fragment Should be separated with spaces',
+      joined.speechOutputForTest.string_);
 });
 
-TEST_F('ChromeVoxOutputE2ETest', 'ComplexDiv', function() {
-  this.runWithLoadedTree(
-      `
+TEST_F('ChromeVoxOutputE2ETest', 'ComplexDiv', async function() {
+  const root = await this.runWithLoadedTree(`
       <div><button>ok</button></div>
-    `,
-      function(root) {
-        const div = root.find({role: RoleType.GENERIC_CONTAINER});
-        const o = new Output().withSpeech(cursors.Range.fromNode(div));
-        assertEquals('ok', o.speechOutputForTest.string_);
-      });
+    `);
+  const div = root.find({role: RoleType.GENERIC_CONTAINER});
+  const o = new Output().withSpeech(cursors.Range.fromNode(div));
+  assertEquals('ok', o.speechOutputForTest.string_);
 });
 
-TEST_F('ChromeVoxOutputE2ETest', 'ContainerFocus', function() {
-  this.runWithLoadedTree(
-      `
+TEST_F('ChromeVoxOutputE2ETest', 'ContainerFocus', async function() {
+  const root = await this.runWithLoadedTree(`
       <div role="grid">
         <div role="row" tabindex=0 aria-label="start"></div>
         <div role="row" tabindex=0 aria-label="end"></div>
       </div>
-    `,
-      function(root) {
-        const r1 = cursors.Range.fromNode(root.firstChild.firstChild);
-        const r2 = cursors.Range.fromNode(root.firstChild.lastChild);
-        assertEquals(
-            'start|Row',
-            new Output().withSpeech(r1, r2).speechOutputForTest.string_);
-      });
+    `);
+  const r1 = cursors.Range.fromNode(root.firstChild.firstChild);
+  const r2 = cursors.Range.fromNode(root.firstChild.lastChild);
+  assertEquals(
+      'start|Row', new Output().withSpeech(r1, r2).speechOutputForTest.string_);
 });
 
-TEST_F('ChromeVoxOutputE2ETest', 'BraileWhitespace', function() {
-  this.runWithLoadedTree(
-      `
+TEST_F('ChromeVoxOutputE2ETest', 'BraileWhitespace', async function() {
+  const root = await this.runWithLoadedTree(`
     <p>this is a <em>test</em>of emphasized text</p>
-  `,
-      function(root) {
-        const start = root.firstChild.firstChild;
-        const end = root.firstChild.lastChild;
-        const range = new cursors.Range(
-            cursors.Cursor.fromNode(start), cursors.Cursor.fromNode(end));
-        const o = new Output().withBraille(range, null, 'navigate');
-        checkBrailleOutput(
-            'this is a test of emphasized text',
-            [
-              {value: new OutputNodeSpan(start), start: 0, end: 10}, {
-                value: new OutputNodeSpan(start.nextSibling),
-                start: 10,
-                end: 14
-              },
-              {value: new OutputNodeSpan(end), start: 15, end: 33}
-            ],
-            o);
-      });
+  `);
+  const start = root.firstChild.firstChild;
+  const end = root.firstChild.lastChild;
+  const range = new cursors.Range(
+      cursors.Cursor.fromNode(start), cursors.Cursor.fromNode(end));
+  const o = new Output().withBraille(range, null, 'navigate');
+  checkBrailleOutput(
+      'this is a test of emphasized text',
+      [
+        {value: new OutputNodeSpan(start), start: 0, end: 10},
+        {value: new OutputNodeSpan(start.nextSibling), start: 10, end: 14},
+        {value: new OutputNodeSpan(end), start: 15, end: 33}
+      ],
+      o);
 });
 
-TEST_F('ChromeVoxOutputE2ETest', 'BrailleAncestry', function() {
-  this.runWithLoadedTree(
-      `
+TEST_F('ChromeVoxOutputE2ETest', 'BrailleAncestry', async function() {
+  const root = await this.runWithLoadedTree(`
     <ul><li><a href="#">test</a></li></ul>
-  `,
-      function(root) {
-        const link = root.find({role: RoleType.LINK});
-        // The 'inlineTextBox' found from root would return the inlineTextBox of
-        // the list marker. Here we want the link's inlineTextBox.
-        const text = link.find({role: RoleType.INLINE_TEXT_BOX});
-        const listItem = root.find({role: RoleType.LIST_ITEM});
-        const list = root.find({role: RoleType.LIST});
-        let range = cursors.Range.fromNode(text);
-        let o = new Output().withBraille(range, null, 'navigate');
-        checkBrailleOutput(
-            'test lnk lstitm lst end',
-            [
-              {value: new OutputNodeSpan(text), start: 0, end: 4},
-              {value: new OutputNodeSpan(link), start: 5, end: 8},
-              {value: new OutputNodeSpan(listItem), start: 9, end: 15},
-              {value: new OutputNodeSpan(list), start: 16, end: 23}
-            ],
-            o);
+  `);
+  const link = root.find({role: RoleType.LINK});
+  // The 'inlineTextBox' found from root would return the inlineTextBox of
+  // the list marker. Here we want the link's inlineTextBox.
+  const text = link.find({role: RoleType.INLINE_TEXT_BOX});
+  const listItem = root.find({role: RoleType.LIST_ITEM});
+  const list = root.find({role: RoleType.LIST});
+  let range = cursors.Range.fromNode(text);
+  let o = new Output().withBraille(range, null, 'navigate');
+  checkBrailleOutput(
+      'test lnk lstitm lst end',
+      [
+        {value: new OutputNodeSpan(text), start: 0, end: 4},
+        {value: new OutputNodeSpan(link), start: 5, end: 8},
+        {value: new OutputNodeSpan(listItem), start: 9, end: 15},
+        {value: new OutputNodeSpan(list), start: 16, end: 23}
+      ],
+      o);
 
-        // Now, test the "bullet" which comes before the above.
-        const bullet = root.find({role: RoleType.LIST_MARKER});
-        range = cursors.Range.fromNode(bullet);
-        o = new Output().withBraille(range, null, 'navigate');
-        checkBrailleOutput(
-            '\u2022 lstitm lst +1',
-            [
-              {value: new OutputNodeSpan(bullet), start: 0, end: 2},
-              {value: new OutputNodeSpan(listItem), start: 2, end: 8},
-              {value: new OutputNodeSpan(list), start: 9, end: 15}
-            ],
-            o);
-      });
+  // Now, test the "bullet" which comes before the above.
+  const bullet = root.find({role: RoleType.LIST_MARKER});
+  range = cursors.Range.fromNode(bullet);
+  o = new Output().withBraille(range, null, 'navigate');
+  checkBrailleOutput(
+      '\u2022 lstitm lst +1',
+      [
+        {value: new OutputNodeSpan(bullet), start: 0, end: 2},
+        {value: new OutputNodeSpan(listItem), start: 2, end: 8},
+        {value: new OutputNodeSpan(list), start: 9, end: 15}
+      ],
+      o);
 });
 
-TEST_F('ChromeVoxOutputE2ETest', 'RangeOutput', function() {
-  this.runWithLoadedTree(
-      `
+TEST_F('ChromeVoxOutputE2ETest', 'RangeOutput', async function() {
+  const root = await this.runWithLoadedTree(`
     <div role="slider" aria-valuemin="1" aria-valuemax="10" aria-valuenow="2"
                        aria-label="volume"></div>
     <progress aria-valuemin="1" aria-valuemax="10"
@@ -915,75 +845,64 @@
            aria-label="volume"></meter>
     <div role="spinbutton" aria-valuemin="1" aria-valuemax="10"
                            aria-valuenow="2" aria-label="volume"></div>
-  `,
-      function(root) {
-        let obj = root.find({role: RoleType.SLIDER});
-        let o =
-            new Output().withoutHints().withSpeech(cursors.Range.fromNode(obj));
-        checkSpeechOutput(
-            'volume|Slider|2|Min 1|Max 10',
-            [
-              {value: 'name', start: 0, end: 6},
-              {value: new OutputEarconAction('SLIDER'), start: 0, end: 6},
-              {value: 'role', start: 7, end: 13},
-              {value: 'value', start: 14, end: 15}
-            ],
-            o);
+  `);
+  let obj = root.find({role: RoleType.SLIDER});
+  let o = new Output().withoutHints().withSpeech(cursors.Range.fromNode(obj));
+  checkSpeechOutput(
+      'volume|Slider|2|Min 1|Max 10',
+      [
+        {value: 'name', start: 0, end: 6},
+        {value: new OutputEarconAction('SLIDER'), start: 0, end: 6},
+        {value: 'role', start: 7, end: 13}, {value: 'value', start: 14, end: 15}
+      ],
+      o);
 
-        obj = root.find({role: RoleType.PROGRESS_INDICATOR});
-        o = new Output().withoutHints().withSpeech(cursors.Range.fromNode(obj));
-        checkSpeechOutput(
-            'volume|Progress indicator|2|Min 1|Max 10',
-            [
-              {value: 'name', start: 0, end: 6},
-              {value: 'role', start: 7, end: 25},
-              {value: 'value', start: 26, end: 27}
-            ],
-            o);
+  obj = root.find({role: RoleType.PROGRESS_INDICATOR});
+  o = new Output().withoutHints().withSpeech(cursors.Range.fromNode(obj));
+  checkSpeechOutput(
+      'volume|Progress indicator|2|Min 1|Max 10',
+      [
+        {value: 'name', start: 0, end: 6}, {value: 'role', start: 7, end: 25},
+        {value: 'value', start: 26, end: 27}
+      ],
+      o);
 
-        obj = root.find({role: RoleType.METER});
-        o = new Output().withoutHints().withSpeech(cursors.Range.fromNode(obj));
-        checkSpeechOutput(
-            'volume|Meter|2|Min 1|Max 10',
-            [
-              {value: 'name', start: 0, end: 6},
-              {value: 'role', start: 7, end: 12},
-              {value: 'value', start: 13, end: 14}
-            ],
-            o);
+  obj = root.find({role: RoleType.METER});
+  o = new Output().withoutHints().withSpeech(cursors.Range.fromNode(obj));
+  checkSpeechOutput(
+      'volume|Meter|2|Min 1|Max 10',
+      [
+        {value: 'name', start: 0, end: 6}, {value: 'role', start: 7, end: 12},
+        {value: 'value', start: 13, end: 14}
+      ],
+      o);
 
-        obj = root.find({role: RoleType.SPIN_BUTTON});
-        o = new Output().withoutHints().withSpeech(cursors.Range.fromNode(obj));
-        checkSpeechOutput(
-            'volume|Spin button|2|Min 1|Max 10',
-            [
-              {value: 'name', start: 0, end: 6},
-              {value: new OutputEarconAction('LISTBOX'), start: 0, end: 6},
-              {value: 'role', start: 7, end: 18},
-              {value: 'value', start: 19, end: 20}
-            ],
-            o);
-      });
+  obj = root.find({role: RoleType.SPIN_BUTTON});
+  o = new Output().withoutHints().withSpeech(cursors.Range.fromNode(obj));
+  checkSpeechOutput(
+      'volume|Spin button|2|Min 1|Max 10',
+      [
+        {value: 'name', start: 0, end: 6},
+        {value: new OutputEarconAction('LISTBOX'), start: 0, end: 6},
+        {value: 'role', start: 7, end: 18}, {value: 'value', start: 19, end: 20}
+      ],
+      o);
 });
 
-TEST_F('ChromeVoxOutputE2ETest', 'RoleDescription', function() {
-  this.runWithLoadedTree(
-      `
+TEST_F('ChromeVoxOutputE2ETest', 'RoleDescription', async function() {
+  const root = await this.runWithLoadedTree(`
     <div aria-label="hi" role="button" aria-roledescription="foo"></div>
-  `,
-      function(root) {
-        const obj = root.find({role: RoleType.BUTTON});
-        const o =
-            new Output().withoutHints().withSpeech(cursors.Range.fromNode(obj));
-        checkSpeechOutput(
-            'hi|foo',
-            [
-              {value: 'name', start: 0, end: 2},
-              {value: new OutputEarconAction('BUTTON'), start: 0, end: 2},
-              {value: 'role', start: 3, end: 6}
-            ],
-            o);
-      });
+  `);
+  const obj = root.find({role: RoleType.BUTTON});
+  const o = new Output().withoutHints().withSpeech(cursors.Range.fromNode(obj));
+  checkSpeechOutput(
+      'hi|foo',
+      [
+        {value: 'name', start: 0, end: 2},
+        {value: new OutputEarconAction('BUTTON'), start: 0, end: 2},
+        {value: 'role', start: 3, end: 6}
+      ],
+      o);
 });
 
 SYNC_TEST_F('ChromeVoxOutputE2ETest', 'ValidateCommonProperties', function() {
@@ -1128,53 +1047,45 @@
           missingRole.join(' '));
 });
 
-TEST_F('ChromeVoxOutputE2ETest', 'InlineBraille', function() {
-  this.runWithLoadedTree(
-      `
+TEST_F('ChromeVoxOutputE2ETest', 'InlineBraille', async function() {
+  const root = await this.runWithLoadedTree(`
     <table border=1>
       <tr><td>Name</td><td id="active">Age</td><td>Address</td></tr>
     </table>
-  `,
-      function(root) {
-        const obj = root.find({role: RoleType.CELL});
-        const o =
-            new Output().withRichSpeechAndBraille(cursors.Range.fromNode(obj));
-        assertEquals(
-            'Name|row 1 column 1|Table , 1 by 3',
-            o.speechOutputForTest.string_);
-        assertEquals(
-            'Name r1c1 Age r1c2 Address r1c3', o.brailleOutputForTest.string_);
-      });
+  `);
+  const obj = root.find({role: RoleType.CELL});
+  const o = new Output().withRichSpeechAndBraille(cursors.Range.fromNode(obj));
+  assertEquals(
+      'Name|row 1 column 1|Table , 1 by 3', o.speechOutputForTest.string_);
+  assertEquals(
+      'Name r1c1 Age r1c2 Address r1c3', o.brailleOutputForTest.string_);
 });
 
-TEST_F('ChromeVoxOutputE2ETest', 'TextFieldObeysRoleDescription', function() {
-  this.runWithLoadedTree(
-      `
+TEST_F(
+    'ChromeVoxOutputE2ETest', 'TextFieldObeysRoleDescription',
+    async function() {
+      const root = await this.runWithLoadedTree(`
     <div role="textbox" aria-roledescription="square"></div>
     <div role="region" aria-roledescription="circle"></div>
-  `,
-      function(root) {
-        const text = root.find({role: RoleType.TEXT_FIELD});
+  `);
+      const text = root.find({role: RoleType.TEXT_FIELD});
 
-        // True even though |text| does not have editable state.
-        assertTrue(AutomationPredicate.editText(text));
+      // True even though |text| does not have editable state.
+      assertTrue(AutomationPredicate.editText(text));
 
-        let o =
-            new Output().withRichSpeechAndBraille(cursors.Range.fromNode(text));
-        assertEquals('|square', o.speechOutputForTest.string_);
-        assertEquals('square', o.brailleOutputForTest.string_);
+      let o =
+          new Output().withRichSpeechAndBraille(cursors.Range.fromNode(text));
+      assertEquals('|square', o.speechOutputForTest.string_);
+      assertEquals('square', o.brailleOutputForTest.string_);
 
-        const region = root.find({role: RoleType.REGION});
-        o = new Output().withRichSpeechAndBraille(
-            cursors.Range.fromNode(region));
-        assertEquals('circle', o.speechOutputForTest.string_);
-        assertEquals('circle', o.brailleOutputForTest.string_);
-      });
-});
+      const region = root.find({role: RoleType.REGION});
+      o = new Output().withRichSpeechAndBraille(cursors.Range.fromNode(region));
+      assertEquals('circle', o.speechOutputForTest.string_);
+      assertEquals('circle', o.brailleOutputForTest.string_);
+    });
 
-TEST_F('ChromeVoxOutputE2ETest', 'NestedList', function() {
-  this.runWithLoadedTree(
-      `
+TEST_F('ChromeVoxOutputE2ETest', 'NestedList', async function() {
+  const root = await this.runWithLoadedTree(`
     <ul role="tree">schedule
       <li role="treeitem">wake up
       <li role="treeitem">drink coffee
@@ -1184,335 +1095,306 @@
       </ul>
       <li role="treeitem">cook dinner
     </ul>
-  `,
+  `);
+  const lists = root.findAll({role: RoleType.TREE});
+  const outerList = lists[0];
+  const innerList = lists[1];
 
-      function(root) {
-        const lists = root.findAll({role: RoleType.TREE});
-        const outerList = lists[0];
-        const innerList = lists[1];
+  let el = outerList.children[0];
+  let startRange = cursors.Range.fromNode(el);
+  let o = new Output().withSpeech(startRange, null, 'navigate');
+  assertEquals('schedule|Tree|with 3 items', o.speechOutputForTest.string_);
 
-        let el = outerList.children[0];
-        let startRange = cursors.Range.fromNode(el);
-        let o = new Output().withSpeech(startRange, null, 'navigate');
-        assertEquals(
-            'schedule|Tree|with 3 items', o.speechOutputForTest.string_);
+  el = outerList.children[1];
+  startRange = cursors.Range.fromNode(el);
+  o = new Output().withSpeech(
+      startRange, cursors.Range.fromNode(outerList.children[0]), 'navigate');
+  assertEquals(
+      'wake up|Tree item|Not selected| 1 of 3 | level 1 ',
+      o.speechOutputForTest.string_);
 
-        el = outerList.children[1];
-        startRange = cursors.Range.fromNode(el);
-        o = new Output().withSpeech(
-            startRange, cursors.Range.fromNode(outerList.children[0]),
-            'navigate');
-        assertEquals(
-            'wake up|Tree item|Not selected| 1 of 3 | level 1 ',
-            o.speechOutputForTest.string_);
+  el = outerList.children[2];
+  startRange = cursors.Range.fromNode(el);
+  o = new Output().withSpeech(
+      startRange, cursors.Range.fromNode(outerList.children[0]), 'navigate');
+  assertEquals(
+      'drink coffee|Tree item|Not selected| 2 of 3 | level 1 ',
+      o.speechOutputForTest.string_);
 
-        el = outerList.children[2];
-        startRange = cursors.Range.fromNode(el);
-        o = new Output().withSpeech(
-            startRange, cursors.Range.fromNode(outerList.children[0]),
-            'navigate');
-        assertEquals(
-            'drink coffee|Tree item|Not selected| 2 of 3 | level 1 ',
-            o.speechOutputForTest.string_);
+  el = outerList.children[3];
+  startRange = cursors.Range.fromNode(el);
+  o = new Output().withSpeech(
+      startRange, cursors.Range.fromNode(outerList.children[0]), 'navigate');
+  assertEquals(
+      'cook dinner|Tree item|Not selected| 3 of 3 | level 1 ',
+      o.speechOutputForTest.string_);
 
-        el = outerList.children[3];
-        startRange = cursors.Range.fromNode(el);
-        o = new Output().withSpeech(
-            startRange, cursors.Range.fromNode(outerList.children[0]),
-            'navigate');
-        assertEquals(
-            'cook dinner|Tree item|Not selected| 3 of 3 | level 1 ',
-            o.speechOutputForTest.string_);
+  el = innerList.children[0];
+  startRange = cursors.Range.fromNode(el);
+  o = new Output().withSpeech(
+      startRange, cursors.Range.fromNode(outerList.children[2]), 'navigate');
+  assertEquals('tasks|Tree|with 2 items', o.speechOutputForTest.string_);
 
-        el = innerList.children[0];
-        startRange = cursors.Range.fromNode(el);
-        o = new Output().withSpeech(
-            startRange, cursors.Range.fromNode(outerList.children[2]),
-            'navigate');
-        assertEquals('tasks|Tree|with 2 items', o.speechOutputForTest.string_);
+  el = innerList.children[1];
+  startRange = cursors.Range.fromNode(el);
+  o = new Output().withSpeech(
+      startRange, cursors.Range.fromNode(innerList.children[0]), 'navigate');
+  assertEquals(
+      'meeting|Tree item|Not selected| 1 of 2 | level 2 ',
+      o.speechOutputForTest.string_);
 
-        el = innerList.children[1];
-        startRange = cursors.Range.fromNode(el);
-        o = new Output().withSpeech(
-            startRange, cursors.Range.fromNode(innerList.children[0]),
-            'navigate');
-        assertEquals(
-            'meeting|Tree item|Not selected| 1 of 2 | level 2 ',
-            o.speechOutputForTest.string_);
-
-        el = innerList.children[2];
-        startRange = cursors.Range.fromNode(el);
-        o = new Output().withSpeech(
-            startRange, cursors.Range.fromNode(innerList.children[0]),
-            'navigate');
-        assertEquals(
-            'lunch|Tree item|Not selected| 2 of 2 | level 2 ',
-            o.speechOutputForTest.string_);
-      });
+  el = innerList.children[2];
+  startRange = cursors.Range.fromNode(el);
+  o = new Output().withSpeech(
+      startRange, cursors.Range.fromNode(innerList.children[0]), 'navigate');
+  assertEquals(
+      'lunch|Tree item|Not selected| 2 of 2 | level 2 ',
+      o.speechOutputForTest.string_);
 });
 
-TEST_F('ChromeVoxOutputE2ETest', 'NoTooltipWithNameTitle', function() {
-  this.runWithLoadedTree(
-      `
+TEST_F('ChromeVoxOutputE2ETest', 'NoTooltipWithNameTitle', async function() {
+  const root = await this.runWithLoadedTree(`
     <div title="title"></div>
     <div aria-label="label" title="title"></div>
     <div aria-describedby="desc" title="title"></div>
     <div aria-label="label" aria-describedby="desc" title="title"></div>
     <div aria-label=""></div>
     <p id="desc">describedby</p>
-  `,
-      function(root) {
-        const title = root.children[0];
-        let o = new Output().withSpeech(
-            cursors.Range.fromNode(title), null, 'navigate');
-        assertEqualsJSON(
-            {string_: 'title', spans_: [{value: 'name', start: 0, end: 5}]},
-            o.speechOutputForTest);
+  `);
+  const title = root.children[0];
+  let o =
+      new Output().withSpeech(cursors.Range.fromNode(title), null, 'navigate');
+  assertEqualsJSON(
+      {string_: 'title', spans_: [{value: 'name', start: 0, end: 5}]},
+      o.speechOutputForTest);
 
-        const labelTitle = root.children[1];
-        o = new Output().withSpeech(
-            cursors.Range.fromNode(labelTitle), null, 'navigate');
-        assertEqualsJSON(
-            {
-              string_: 'label|title',
-              spans_: [
-                {value: 'name', start: 0, end: 5},
-                {value: 'description', start: 6, end: 11}
-              ]
-            },
-            o.speechOutputForTest);
+  const labelTitle = root.children[1];
+  o = new Output().withSpeech(
+      cursors.Range.fromNode(labelTitle), null, 'navigate');
+  assertEqualsJSON(
+      {
+        string_: 'label|title',
+        spans_: [
+          {value: 'name', start: 0, end: 5},
+          {value: 'description', start: 6, end: 11}
+        ]
+      },
+      o.speechOutputForTest);
 
-        const describedByTitle = root.children[2];
-        o = new Output().withSpeech(
-            cursors.Range.fromNode(describedByTitle), null, 'navigate');
-        assertEqualsJSON(
-            {
-              string_: 'title|describedby',
-              spans_: [
-                {value: 'name', start: 0, end: 5},
-                {value: 'description', start: 6, end: 17}
-              ]
-            },
-            o.speechOutputForTest);
+  const describedByTitle = root.children[2];
+  o = new Output().withSpeech(
+      cursors.Range.fromNode(describedByTitle), null, 'navigate');
+  assertEqualsJSON(
+      {
+        string_: 'title|describedby',
+        spans_: [
+          {value: 'name', start: 0, end: 5},
+          {value: 'description', start: 6, end: 17}
+        ]
+      },
+      o.speechOutputForTest);
 
-        const labelDescribedByTitle = root.children[3];
-        o = new Output().withSpeech(
-            cursors.Range.fromNode(labelDescribedByTitle), null, 'navigate');
-        assertEqualsJSON(
-            {
-              string_: 'label|describedby',
-              spans_: [
-                {value: 'name', start: 0, end: 5},
-                {value: 'description', start: 6, end: 17}
-              ]
-            },
-            o.speechOutputForTest);
+  const labelDescribedByTitle = root.children[3];
+  o = new Output().withSpeech(
+      cursors.Range.fromNode(labelDescribedByTitle), null, 'navigate');
+  assertEqualsJSON(
+      {
+        string_: 'label|describedby',
+        spans_: [
+          {value: 'name', start: 0, end: 5},
+          {value: 'description', start: 6, end: 17}
+        ]
+      },
+      o.speechOutputForTest);
 
-        // Hijack the 4th node to force tooltip to return a value. This can only
-        // occur on ARC++ where tooltip gets set even if name and description
-        // are both empty.
-        const tooltip = root.children[4];
-        Object.defineProperty(
-            root.children[4], 'tooltip', {get: () => 'tooltip'});
+  // Hijack the 4th node to force tooltip to return a value. This can only
+  // occur on ARC++ where tooltip gets set even if name and description
+  // are both empty.
+  const tooltip = root.children[4];
+  Object.defineProperty(root.children[4], 'tooltip', {get: () => 'tooltip'});
 
-        o = new Output().withSpeech(
-            cursors.Range.fromNode(tooltip), null, 'navigate');
-        assertEqualsJSON(
-            {
-              string_: 'tooltip',
-              spans_: [{value: {'delay': true}, start: 0, end: 7}]
-            },
-            o.speechOutputForTest);
-      });
+  o = new Output().withSpeech(
+      cursors.Range.fromNode(tooltip), null, 'navigate');
+  assertEqualsJSON(
+      {
+        string_: 'tooltip',
+        spans_: [{value: {'delay': true}, start: 0, end: 7}]
+      },
+      o.speechOutputForTest);
 });
 
-TEST_F('ChromeVoxOutputE2ETest', 'InitialSpeechProperties', function() {
-  this.runWithLoadedTree(
-      `
-    <p>test</p>  `,
-      function(root) {
-        // Capture speech properties sent to tts.
-        this.currentProperties = [];
-        ChromeVox.tts.speak = (textString, queueMode, properties) => {
-          this.currentProperties.push(properties);
-        };
+TEST_F('ChromeVoxOutputE2ETest', 'InitialSpeechProperties', async function() {
+  const root = await this.runWithLoadedTree(`
+    <p>test</p>  `);
+  // Capture speech properties sent to tts.
+  this.currentProperties = [];
+  ChromeVox.tts.speak = (textString, queueMode, properties) => {
+    this.currentProperties.push(properties);
+  };
 
-        const o =
-            new Output().withSpeech(cursors.Range.fromNode(root.firstChild));
-        o.go();
-        assertEqualsJSON([{category: TtsCategory.NAV}], this.currentProperties);
-        this.currentProperties = [];
+  const o = new Output().withSpeech(cursors.Range.fromNode(root.firstChild));
+  o.go();
+  assertEqualsJSON([{category: TtsCategory.NAV}], this.currentProperties);
+  this.currentProperties = [];
 
-        o.withInitialSpeechProperties({
-          phoneticCharacters: true,
-          // This should not override existing value.
-          category: TtsCategory.LIVE
-        });
-        o.go();
-        assertEqualsJSON(
-            [{phoneticCharacters: true, category: TtsCategory.NAV}],
-            this.currentProperties);
-      });
+  o.withInitialSpeechProperties({
+    phoneticCharacters: true,
+    // This should not override existing value.
+    category: TtsCategory.LIVE
+  });
+  o.go();
+  assertEqualsJSON(
+      [{phoneticCharacters: true, category: TtsCategory.NAV}],
+      this.currentProperties);
 });
 
-TEST_F('ChromeVoxOutputE2ETest', 'NameOrTextContent', function() {
-  this.runWithLoadedTree(
-      `
+TEST_F('ChromeVoxOutputE2ETest', 'NameOrTextContent', async function() {
+  const root = await this.runWithLoadedTree(`
         <div tabindex=-1>
           <div aria-label="hello there world">
             <p>hello world</p>
           </div>
         </div>
-      `,
-      function(root) {
-        const focusableDiv = root.firstChild;
-        assertEquals(RoleType.GENERIC_CONTAINER, focusableDiv.role);
-        assertEquals(
-            chrome.automation.NameFromType.CONTENTS, focusableDiv.nameFrom);
-        const o = new Output().withSpeech(cursors.Range.fromNode(focusableDiv));
-        assertEquals('hello there world', o.speechOutputForTest.string_);
-      });
+      `);
+  const focusableDiv = root.firstChild;
+  assertEquals(RoleType.GENERIC_CONTAINER, focusableDiv.role);
+  assertEquals(chrome.automation.NameFromType.CONTENTS, focusableDiv.nameFrom);
+  const o = new Output().withSpeech(cursors.Range.fromNode(focusableDiv));
+  assertEquals('hello there world', o.speechOutputForTest.string_);
 });
 
-TEST_F('ChromeVoxOutputE2ETest', 'AriaCurrentHint', function() {
+TEST_F('ChromeVoxOutputE2ETest', 'AriaCurrentHint', async function() {
   const site = `
       <div aria-current="page">Home</div>
       <div aria-current="false">About</div>
       `;
-  this.runWithLoadedTree(site, function(root) {
-    const currentDiv = root.firstChild;
-    assertEquals(
-        chrome.automation.AriaCurrentState.PAGE, currentDiv.ariaCurrentState);
-    const o = new Output().withSpeech(
-        cursors.Range.fromNode(currentDiv), null, 'navigate');
-    assertEquals('Home|Current page', o.speechOutputForTest.string_);
-  });
+  const root = await this.runWithLoadedTree(site);
+  const currentDiv = root.firstChild;
+  assertEquals(
+      chrome.automation.AriaCurrentState.PAGE, currentDiv.ariaCurrentState);
+  const o = new Output().withSpeech(
+      cursors.Range.fromNode(currentDiv), null, 'navigate');
+  assertEquals('Home|Current page', o.speechOutputForTest.string_);
 });
 
-TEST_F('ChromeVoxOutputE2ETest', 'DelayHintVariants', function() {
-  this.runWithLoadedTree(
-      `
+TEST_F('ChromeVoxOutputE2ETest', 'DelayHintVariants', async function() {
+  const root = await this.runWithLoadedTree(`
     <div aria-errormessage="error" aria-invalid="true">OK</div>
     <div id="error" aria-label="error"></div>
-  `,
-      function(root) {
-        const div = root.children[0];
-        const range = cursors.Range.fromNode(div);
+  `);
+  const div = root.children[0];
+  const range = cursors.Range.fromNode(div);
 
-        let o = new Output().withSpeech(range, null, 'navigate');
-        assertEqualsJSON(
-            {string_: 'OK|error', spans_: [{value: 'name', start: 3, end: 8}]},
-            o.speechOutputForTest);
+  let o = new Output().withSpeech(range, null, 'navigate');
+  assertEqualsJSON(
+      {string_: 'OK|error', spans_: [{value: 'name', start: 3, end: 8}]},
+      o.speechOutputForTest);
 
-        // Force a few properties to be set so that hints are triggered.
-        Object.defineProperty(div, 'clickable', {get: () => true});
+  // Force a few properties to be set so that hints are triggered.
+  Object.defineProperty(div, 'clickable', {get: () => true});
 
-        o = new Output().withSpeech(range, null, 'navigate');
-        assertEqualsJSON(
-            {
-              string_: 'OK|error|Press Search+Space to activate',
-              spans_: [
-                {value: 'name', start: 3, end: 8},
-                {value: {delay: true}, start: 9, end: 39}
-              ]
-            },
-            o.speechOutputForTest);
+  o = new Output().withSpeech(range, null, 'navigate');
+  assertEqualsJSON(
+      {
+        string_: 'OK|error|Press Search+Space to activate',
+        spans_: [
+          {value: 'name', start: 3, end: 8},
+          {value: {delay: true}, start: 9, end: 39}
+        ]
+      },
+      o.speechOutputForTest);
 
-        Object.defineProperty(div, 'placeholder', {get: () => 'placeholder'});
-        o = new Output().withSpeech(range, null, 'navigate');
-        assertEqualsJSON(
-            {
-              string_: 'OK|error|placeholder|Press Search+Space to activate',
-              spans_: [
-                {value: 'name', start: 3, end: 8},
-                {value: {delay: true}, start: 9, end: 20}, {start: 21, end: 51}
-              ]
-            },
-            o.speechOutputForTest);
-      });
+  Object.defineProperty(div, 'placeholder', {get: () => 'placeholder'});
+  o = new Output().withSpeech(range, null, 'navigate');
+  assertEqualsJSON(
+      {
+        string_: 'OK|error|placeholder|Press Search+Space to activate',
+        spans_: [
+          {value: 'name', start: 3, end: 8},
+          {value: {delay: true}, start: 9, end: 20}, {start: 21, end: 51}
+        ]
+      },
+      o.speechOutputForTest);
 });
 
-TEST_F('ChromeVoxOutputE2ETest', 'WithoutFocusRing', function() {
+TEST_F('ChromeVoxOutputE2ETest', 'WithoutFocusRing', async function() {
   const site = `<button></button>`;
-  this.runWithLoadedTree(site, function(root) {
-    let called = false;
-    ChromeVoxState.instance.setFocusBounds = this.newCallback(() => {
-      called = true;
-    });
-
-    const button = root.find({role: RoleType.BUTTON});
-
-    // Triggers drawing of the focus ring.
-    new Output().withSpeech(cursors.Range.fromNode(button)).go();
-    assertTrue(called);
-    called = false;
-
-    // Does not trigger drawing of the focus ring.
-    new Output()
-        .withSpeech(cursors.Range.fromNode(button))
-        .withoutFocusRing()
-        .go();
-    assertFalse(called);
+  const root = await this.runWithLoadedTree(site);
+  let called = false;
+  ChromeVoxState.instance.setFocusBounds = this.newCallback(() => {
+    called = true;
   });
+
+  const button = root.find({role: RoleType.BUTTON});
+
+  // Triggers drawing of the focus ring.
+  new Output().withSpeech(cursors.Range.fromNode(button)).go();
+  assertTrue(called);
+  called = false;
+
+  // Does not trigger drawing of the focus ring.
+  new Output()
+      .withSpeech(cursors.Range.fromNode(button))
+      .withoutFocusRing()
+      .go();
+  assertFalse(called);
 });
 
-TEST_F('ChromeVoxOutputE2ETest', 'ARCCheckbox', function() {
-  this.runWithLoadedTree('<input type="checkbox">', function(root) {
-    const checkbox = root.firstChild.firstChild;
+TEST_F('ChromeVoxOutputE2ETest', 'ARCCheckbox', async function() {
+  const root = await this.runWithLoadedTree('<input type="checkbox">');
+  const checkbox = root.firstChild.firstChild;
 
-    Object.defineProperty(
-        checkbox, 'checkedStateDescription',
-        {get: () => 'checked state description'});
-    const range = cursors.Range.fromNode(checkbox);
-    const o = new Output().withoutHints().withSpeechAndBraille(
-        range, null, 'navigate');
-    checkSpeechOutput(
-        '|Check box|checked state description',
-        [
-          {value: new OutputEarconAction('CHECK_OFF'), start: 0, end: 0},
-          {value: 'role', start: 1, end: 10},
-          {value: 'checkedStateDescription', start: 11, end: 36}
-        ],
-        o);
-  });
+  Object.defineProperty(
+      checkbox, 'checkedStateDescription',
+      {get: () => 'checked state description'});
+  const range = cursors.Range.fromNode(checkbox);
+  const o =
+      new Output().withoutHints().withSpeechAndBraille(range, null, 'navigate');
+  checkSpeechOutput(
+      '|Check box|checked state description',
+      [
+        {value: new OutputEarconAction('CHECK_OFF'), start: 0, end: 0},
+        {value: 'role', start: 1, end: 10},
+        {value: 'checkedStateDescription', start: 11, end: 36}
+      ],
+      o);
 });
 
-TEST_F('ChromeVoxOutputE2ETest', 'ARCCustomAction', function() {
-  this.runWithLoadedTree('<p>test</p>', function(root) {
-    const actionable = root.firstChild.firstChild;
-    Object.defineProperty(actionable, 'customActions', {
-      get: () => [{id: 0, description: 'custom action description'}],
-    });
-    const range = cursors.Range.fromNode(actionable);
-    const o = new Output().withSpeechAndBraille(range, null, 'navigate');
-    checkSpeechOutput(
-        'test|Actions available. Press Search+Ctrl+A to view',
-        [
-          {value: 'name', start: 0, end: 4},
-          {value: {delay: true}, start: 5, end: 51}
-        ],
-        o);
+TEST_F('ChromeVoxOutputE2ETest', 'ARCCustomAction', async function() {
+  const root = await this.runWithLoadedTree('<p>test</p>');
+  const actionable = root.firstChild.firstChild;
+  Object.defineProperty(actionable, 'customActions', {
+    get: () => [{id: 0, description: 'custom action description'}],
   });
+  const range = cursors.Range.fromNode(actionable);
+  const o = new Output().withSpeechAndBraille(range, null, 'navigate');
+  checkSpeechOutput(
+      'test|Actions available. Press Search+Ctrl+A to view',
+      [
+        {value: 'name', start: 0, end: 4},
+        {value: {delay: true}, start: 5, end: 51}
+      ],
+      o);
 });
 
-TEST_F('ChromeVoxOutputE2ETest', 'ContextOrder', function() {
+TEST_F('ChromeVoxOutputE2ETest', 'ContextOrder', async function() {
   this.resetContextualOutput();
-  this.runWithLoadedTree('<p>test</p><div role="menu">a</div>', function(root) {
-    let o = new Output().withSpeech(cursors.Range.fromNode(root));
-    assertEquals('last', o.contextOrder_);
+  const root =
+      await this.runWithLoadedTree('<p>test</p><div role="menu">a</div>');
+  let o = new Output().withSpeech(cursors.Range.fromNode(root));
+  assertEquals('last', o.contextOrder_);
 
-    const p = root.find({role: RoleType.PARAGRAPH});
-    const menu = root.find({role: RoleType.MENU});
-    o = new Output().withSpeech(
-        cursors.Range.fromNode(p), cursors.Range.fromNode(menu));
-    assertEquals('last', o.contextOrder_);
+  const p = root.find({role: RoleType.PARAGRAPH});
+  const menu = root.find({role: RoleType.MENU});
+  o = new Output().withSpeech(
+      cursors.Range.fromNode(p), cursors.Range.fromNode(menu));
+  assertEquals('last', o.contextOrder_);
 
-    o = new Output().withSpeech(
-        cursors.Range.fromNode(menu), cursors.Range.fromNode(p));
-    assertEquals('first', o.contextOrder_);
+  o = new Output().withSpeech(
+      cursors.Range.fromNode(menu), cursors.Range.fromNode(p));
+  assertEquals('first', o.contextOrder_);
 
-    o = new Output().withSpeech(
-        cursors.Range.fromNode(menu.firstChild), cursors.Range.fromNode(p));
-    assertEquals('first', o.contextOrder_);
-  });
+  o = new Output().withSpeech(
+      cursors.Range.fromNode(menu.firstChild), cursors.Range.fromNode(p));
+  assertEquals('first', o.contextOrder_);
 });
diff --git a/chrome/browser/resources/chromeos/accessibility/chromevox/background/portals_test.js b/chrome/browser/resources/chromeos/accessibility/chromevox/background/portals_test.js
index ee8d617..64e3113 100644
--- a/chrome/browser/resources/chromeos/accessibility/chromevox/background/portals_test.js
+++ b/chrome/browser/resources/chromeos/accessibility/chromevox/background/portals_test.js
@@ -64,44 +64,39 @@
   }
 };
 
-TEST_F('ChromeVoxPortalsTest', 'ShouldFocusPortal', function() {
-  this.runWithLoadedTree(
-      undefined, function(root) {
-        const portal = root.find({role: RoleType.PORTAL});
-        const button = root.find({role: RoleType.BUTTON});
-        assertEquals(RoleType.PORTAL, portal.role);
-        assertEquals(RoleType.BUTTON, button.role);
+TEST_F('ChromeVoxPortalsTest', 'ShouldFocusPortal', async function() {
+  const root = await this.runWithLoadedTree(null, {
+    url: `${testRunnerParams.testServerBaseUrl}portal/portal-and-button.html`
+  });
+  const portal = root.find({role: RoleType.PORTAL});
+  const button = root.find({role: RoleType.BUTTON});
+  assertEquals(RoleType.PORTAL, portal.role);
+  assertEquals(RoleType.BUTTON, button.role);
 
-        const afterPortalIsReady = this.newCallback(() => {
-          const chromeVoxState = ChromeVoxState.instance;
-          portal.addEventListener(EventType.FOCUS, this.newCallback(function() {
-            assertEquals(portal, chromeVoxState.currentRange.start.node);
-            // test is done.
-          }));
-          assertEquals(button, chromeVoxState.currentRange.start.node);
-          doCmd('nextObject')();
-        });
+  const afterPortalIsReady = this.newCallback(() => {
+    const chromeVoxState = ChromeVoxState.instance;
+    portal.addEventListener(EventType.FOCUS, this.newCallback(function() {
+      assertEquals(portal, chromeVoxState.currentRange.start.node);
+      // test is done.
+    }));
+    assertEquals(button, chromeVoxState.currentRange.start.node);
+    doCmd('nextObject')();
+  });
 
-        button.focus();
-        button.addEventListener(
-            EventType.FOCUS,
-            () => this.waitForPortal(portal).then(afterPortalIsReady));
-      }.bind(this), {
-        url:
-            `${testRunnerParams.testServerBaseUrl}portal/portal-and-button.html`
-      });
+  button.focus();
+  button.addEventListener(
+      EventType.FOCUS,
+      () => this.waitForPortal(portal).then(afterPortalIsReady));
 });
 
-TEST_F('ChromeVoxPortalsTest', 'PortalName', function() {
-  this.runWithLoadedTree(
-      undefined, function(root) {
-        const portal = root.find({role: RoleType.PORTAL});
-        assertEquals(RoleType.PORTAL, portal.role);
-        this.waitForPortal(portal).then(this.newCallback(() => {
-          assertTrue(portal.firstChild.docLoaded);
-          assertEquals(portal.name, 'some text');
-        }));
-      }.bind(this), {
-        url: `${testRunnerParams.testServerBaseUrl}portal/portal-with-text.html`
-      });
+TEST_F('ChromeVoxPortalsTest', 'PortalName', async function() {
+  const root = await this.runWithLoadedTree(null, {
+    url: `${testRunnerParams.testServerBaseUrl}portal/portal-with-text.html`
+  });
+  const portal = root.find({role: RoleType.PORTAL});
+  assertEquals(RoleType.PORTAL, portal.role);
+  this.waitForPortal(portal).then(this.newCallback(() => {
+    assertTrue(portal.firstChild.docLoaded);
+    assertEquals(portal.name, 'some text');
+  }));
 });
diff --git a/chrome/browser/resources/chromeos/accessibility/chromevox/background/settings_test.js b/chrome/browser/resources/chromeos/accessibility/chromevox/background/settings_test.js
index 55883fee..956a2ae 100644
--- a/chrome/browser/resources/chromeos/accessibility/chromevox/background/settings_test.js
+++ b/chrome/browser/resources/chromeos/accessibility/chromevox/background/settings_test.js
@@ -29,59 +29,65 @@
   `);
     super.testGenPreamble();
   }
+
+  /** @override */
+  async setUpDeferred() {
+    await super.setUpDeferred();
+    await importModule('AbstractTts', '/chromevox/common/abstract_tts.js');
+  }
 };
 
 TEST_F(
-    'ChromeVoxSettingsPagesTest', 'TtsRateCommandOnSettingsPage', function() {
+    'ChromeVoxSettingsPagesTest', 'TtsRateCommandOnSettingsPage',
+    async function() {
       const realTts = ChromeVox.tts;
       const mockFeedback = this.createMockFeedback();
-      this.runWithLoadedTree(`unused`, function() {
-        const increaseRate = realTts.increaseOrDecreaseProperty.bind(
-            realTts, AbstractTts.RATE, true);
-        const decreaseRate = realTts.increaseOrDecreaseProperty.bind(
-            realTts, AbstractTts.RATE, false);
+      await this.runWithLoadedTree(`unused`);
+      const increaseRate = realTts.increaseOrDecreaseProperty.bind(
+          realTts, AbstractTts.RATE, true);
+      const decreaseRate = realTts.increaseOrDecreaseProperty.bind(
+          realTts, AbstractTts.RATE, false);
 
-        mockFeedback.call(doCmd('showTtsSettings'))
-            .expectSpeech(
-                /(Settings)|(Text-to-Speech voice settings subpage back button)/)
+      mockFeedback.call(doCmd('showTtsSettings'))
+          .expectSpeech(
+              /(Settings)|(Text-to-Speech voice settings subpage back button)/)
 
-            // ChromeVox presents a 0% to 100% scale.
-            // Ensure we have the default rate.
-            .call(
-                () => chrome.settingsPrivate.setPref(
-                    'settings.tts.speech_rate', 1.0))
+          // ChromeVox presents a 0% to 100% scale.
+          // Ensure we have the default rate.
+          .call(
+              () => chrome.settingsPrivate.setPref(
+                  'settings.tts.speech_rate', 1.0))
 
-            .call(increaseRate)
-            .expectSpeech('Rate 19 percent')
-            .call(increaseRate)
-            .expectSpeech('Rate 21 percent')
+          .call(increaseRate)
+          .expectSpeech('Rate 19 percent')
+          .call(increaseRate)
+          .expectSpeech('Rate 21 percent')
 
-            // Speed things up...
-            .call(
-                () => chrome.settingsPrivate.setPref(
-                    'settings.tts.speech_rate', 4.9))
-            .expectSpeech('Rate 98 percent')
-            .call(increaseRate)
-            .expectSpeech('Rate 100 percent')
+          // Speed things up...
+          .call(
+              () => chrome.settingsPrivate.setPref(
+                  'settings.tts.speech_rate', 4.9))
+          .expectSpeech('Rate 98 percent')
+          .call(increaseRate)
+          .expectSpeech('Rate 100 percent')
 
-            .call(decreaseRate)
-            .expectSpeech('Rate 98 percent')
-            .call(decreaseRate)
-            .expectSpeech('Rate 96 percent')
+          .call(decreaseRate)
+          .expectSpeech('Rate 98 percent')
+          .call(decreaseRate)
+          .expectSpeech('Rate 96 percent')
 
-            // Slow things down...
-            .call(
-                () => chrome.settingsPrivate.setPref(
-                    'settings.tts.speech_rate', 0.3))
-            .expectSpeech('Rate 2 percent')
-            .call(decreaseRate)
-            .expectSpeech('Rate 0 percent')
+          // Slow things down...
+          .call(
+              () => chrome.settingsPrivate.setPref(
+                  'settings.tts.speech_rate', 0.3))
+          .expectSpeech('Rate 2 percent')
+          .call(decreaseRate)
+          .expectSpeech('Rate 0 percent')
 
-            .call(increaseRate)
-            .expectSpeech('Rate 2 percent')
-            .call(increaseRate)
-            .expectSpeech('Rate 4 percent')
+          .call(increaseRate)
+          .expectSpeech('Rate 2 percent')
+          .call(increaseRate)
+          .expectSpeech('Rate 4 percent')
 
-            .replay();
-      });
+          .replay();
     });
diff --git a/chrome/browser/resources/chromeos/accessibility/chromevox/background/smart_sticky_mode_test.js b/chrome/browser/resources/chromeos/accessibility/chromevox/background/smart_sticky_mode_test.js
index 05824b3e..83f737c 100644
--- a/chrome/browser/resources/chromeos/accessibility/chromevox/background/smart_sticky_mode_test.js
+++ b/chrome/browser/resources/chromeos/accessibility/chromevox/background/smart_sticky_mode_test.js
@@ -44,161 +44,154 @@
   }
 };
 
-TEST_F('ChromeVoxSmartStickyModeTest', 'PossibleRangeTypes', function() {
-  this.runWithLoadedTree(this.relationsDoc, function(root) {
-    const [p, input, textarea, contenteditable, ul1, ul2] = root.children;
+TEST_F('ChromeVoxSmartStickyModeTest', 'PossibleRangeTypes', async function() {
+  const root = await this.runWithLoadedTree(this.relationsDoc);
+  const [p, input, textarea, contenteditable, ul1, ul2] = root.children;
 
-    // First, turn on sticky mode and try changing range to various parts of
-    // the document.
-    ChromeVoxBackground.setPref(
-        'sticky', true /* value */, true /* announce */);
-    this.assertDidTurnOffForNode(input);
-    this.assertDidTurnOffForNode(textarea);
-    this.assertDidNotTurnOffForNode(p);
-    this.assertDidTurnOffForNode(contenteditable);
-    this.assertDidTurnOffForNode(ul1);
-    this.assertDidNotTurnOffForNode(p);
-    this.assertDidTurnOffForNode(ul2);
-    this.assertDidTurnOffForNode(ul1.firstChild);
-    this.assertDidNotTurnOffForNode(ul1.parent);
-    this.assertDidNotTurnOffForNode(ul2.parent);
-    this.assertDidNotTurnOffForNode(p);
-    this.assertDidTurnOffForNode(ul2.firstChild);
-    this.assertDidNotTurnOffForNode(p);
-    this.assertDidNotTurnOffForNode(contenteditable.parent);
-    this.assertDidTurnOffForNode(contenteditable.find({role: 'heading'}));
-    this.assertDidTurnOffForNode(contenteditable.find({role: 'inlineTextBox'}));
-  });
+  // First, turn on sticky mode and try changing range to various parts of
+  // the document.
+  ChromeVoxBackground.setPref('sticky', true /* value */, true /* announce */);
+  this.assertDidTurnOffForNode(input);
+  this.assertDidTurnOffForNode(textarea);
+  this.assertDidNotTurnOffForNode(p);
+  this.assertDidTurnOffForNode(contenteditable);
+  this.assertDidTurnOffForNode(ul1);
+  this.assertDidNotTurnOffForNode(p);
+  this.assertDidTurnOffForNode(ul2);
+  this.assertDidTurnOffForNode(ul1.firstChild);
+  this.assertDidNotTurnOffForNode(ul1.parent);
+  this.assertDidNotTurnOffForNode(ul2.parent);
+  this.assertDidNotTurnOffForNode(p);
+  this.assertDidTurnOffForNode(ul2.firstChild);
+  this.assertDidNotTurnOffForNode(p);
+  this.assertDidNotTurnOffForNode(contenteditable.parent);
+  this.assertDidTurnOffForNode(contenteditable.find({role: 'heading'}));
+  this.assertDidTurnOffForNode(contenteditable.find({role: 'inlineTextBox'}));
 });
 
 TEST_F(
-    'ChromeVoxSmartStickyModeTest', 'UserPressesStickyModeCommand', function() {
-      this.runWithLoadedTree(this.relationsDoc, function(root) {
-        const [p, input, textarea, contenteditable, ul1, ul2] = root.children;
-        ChromeVoxBackground.setPref(
-            'sticky', true /* value */, true /* announce */);
+    'ChromeVoxSmartStickyModeTest', 'UserPressesStickyModeCommand',
+    async function() {
+      const root = await this.runWithLoadedTree(this.relationsDoc);
+      const [p, input, textarea, contenteditable, ul1, ul2] = root.children;
+      ChromeVoxBackground.setPref(
+          'sticky', true /* value */, true /* announce */);
 
-        // Mix in calls to turn on / off sticky mode while moving the range
-        // around.
-        this.assertDidTurnOffForNode(input);
-        this.ssm_.onStickyModeCommand(cursors.Range.fromNode(input));
-        this.assertDidNotTurnOffForNode(input);
-        this.ssm_.onStickyModeCommand(cursors.Range.fromNode(input));
-        this.assertDidNotTurnOffForNode(input);
-        this.assertDidNotTurnOffForNode(input.firstChild);
-        this.assertDidNotTurnOffForNode(p);
+      // Mix in calls to turn on / off sticky mode while moving the range
+      // around.
+      this.assertDidTurnOffForNode(input);
+      this.ssm_.onStickyModeCommand(cursors.Range.fromNode(input));
+      this.assertDidNotTurnOffForNode(input);
+      this.ssm_.onStickyModeCommand(cursors.Range.fromNode(input));
+      this.assertDidNotTurnOffForNode(input);
+      this.assertDidNotTurnOffForNode(input.firstChild);
+      this.assertDidNotTurnOffForNode(p);
 
-        // Make sure sticky mode is on again. This call doesn't impact our
-        // instance of SmartStickyMode.
-        ChromeVoxBackground.setPref(
-            'sticky', true /* value */, true /* announce */);
+      // Make sure sticky mode is on again. This call doesn't impact our
+      // instance of SmartStickyMode.
+      ChromeVoxBackground.setPref(
+          'sticky', true /* value */, true /* announce */);
 
-        // Mix in more sticky mode user commands and move to related nodes.
-        this.assertDidTurnOffForNode(contenteditable);
-        this.assertDidTurnOffForNode(ul2);
-        this.ssm_.onStickyModeCommand(cursors.Range.fromNode(ul2));
-        this.assertDidNotTurnOffForNode(ul2);
-        this.assertDidNotTurnOffForNode(ul2.firstChild);
-        this.assertDidNotTurnOffForNode(contenteditable);
-        this.ssm_.onStickyModeCommand(cursors.Range.fromNode(input));
-        this.assertDidNotTurnOffForNode(ul2);
-        this.assertDidNotTurnOffForNode(ul2.firstChild);
-        this.assertDidNotTurnOffForNode(contenteditable);
+      // Mix in more sticky mode user commands and move to related nodes.
+      this.assertDidTurnOffForNode(contenteditable);
+      this.assertDidTurnOffForNode(ul2);
+      this.ssm_.onStickyModeCommand(cursors.Range.fromNode(ul2));
+      this.assertDidNotTurnOffForNode(ul2);
+      this.assertDidNotTurnOffForNode(ul2.firstChild);
+      this.assertDidNotTurnOffForNode(contenteditable);
+      this.ssm_.onStickyModeCommand(cursors.Range.fromNode(input));
+      this.assertDidNotTurnOffForNode(ul2);
+      this.assertDidNotTurnOffForNode(ul2.firstChild);
+      this.assertDidNotTurnOffForNode(contenteditable);
 
-        // Finally, verify sticky mode isn't impacted on non-editables.
-        this.assertDidNotTurnOffForNode(p);
-        this.ssm_.onStickyModeCommand(cursors.Range.fromNode(p));
-        this.assertDidNotTurnOffForNode(p);
-        this.ssm_.onStickyModeCommand(cursors.Range.fromNode(p));
-        this.assertDidNotTurnOffForNode(p);
-      });
+      // Finally, verify sticky mode isn't impacted on non-editables.
+      this.assertDidNotTurnOffForNode(p);
+      this.ssm_.onStickyModeCommand(cursors.Range.fromNode(p));
+      this.assertDidNotTurnOffForNode(p);
+      this.ssm_.onStickyModeCommand(cursors.Range.fromNode(p));
+      this.assertDidNotTurnOffForNode(p);
     });
 
 TEST_F(
-    'ChromeVoxSmartStickyModeTest', 'SmartStickyModeJumpCommands', function() {
+    'ChromeVoxSmartStickyModeTest', 'SmartStickyModeJumpCommands',
+    async function() {
       const mockFeedback = this.createMockFeedback();
-      this.runWithLoadedTree(
-          `
+      const root = await this.runWithLoadedTree(`
         <p>start</p>
         <input type="text"></input>
         <button>end</button>
-      `,
-          function(root) {
-            mockFeedback.call(doCmd('toggleStickyMode'))
-                .expectSpeech('Sticky mode enabled')
-                .call(doCmd('nextFormField'))
-                .expectSpeech('Edit text')
-                .call(() => assertTrue(ChromeVox.isStickyModeOn()))
-                .call(doCmd('nextFormField'))
-                .expectSpeech('Button')
-                .call(doCmd('previousFormField'))
-                .expectSpeech('Edit text')
-                .call(() => assertTrue(ChromeVox.isStickyModeOn()))
-                .call(doCmd('previousObject'))
-                .expectSpeech('start')
-                .call(doCmd('nextEditText'))
-                .expectSpeech('Edit text')
-                .call(() => assertTrue(ChromeVox.isStickyModeOn()))
-                .call(doCmd('nextObject'))
-                .expectSpeech('Button')
-                .call(doCmd('previousEditText'))
-                .expectSpeech('Edit text')
-                .call(() => assertTrue(ChromeVox.isStickyModeOn()))
-                .call(doCmd('nextObject'))
-                .expectSpeech('Button')
-                .call(doCmd('previousObject'))
-                .expectSpeech('Sticky mode disabled')
-                .expectSpeech('Edit text')
-                .call(() => assertFalse(ChromeVox.isStickyModeOn()))
-                .replay();
-          });
+      `);
+      mockFeedback.call(doCmd('toggleStickyMode'))
+          .expectSpeech('Sticky mode enabled')
+          .call(doCmd('nextFormField'))
+          .expectSpeech('Edit text')
+          .call(() => assertTrue(ChromeVox.isStickyModeOn()))
+          .call(doCmd('nextFormField'))
+          .expectSpeech('Button')
+          .call(doCmd('previousFormField'))
+          .expectSpeech('Edit text')
+          .call(() => assertTrue(ChromeVox.isStickyModeOn()))
+          .call(doCmd('previousObject'))
+          .expectSpeech('start')
+          .call(doCmd('nextEditText'))
+          .expectSpeech('Edit text')
+          .call(() => assertTrue(ChromeVox.isStickyModeOn()))
+          .call(doCmd('nextObject'))
+          .expectSpeech('Button')
+          .call(doCmd('previousEditText'))
+          .expectSpeech('Edit text')
+          .call(() => assertTrue(ChromeVox.isStickyModeOn()))
+          .call(doCmd('nextObject'))
+          .expectSpeech('Button')
+          .call(doCmd('previousObject'))
+          .expectSpeech('Sticky mode disabled')
+          .expectSpeech('Edit text')
+          .call(() => assertFalse(ChromeVox.isStickyModeOn()))
+          .replay();
     });
 
-TEST_F('ChromeVoxSmartStickyModeTest', 'SmartStickyModeEarcons', function() {
-  const mockFeedback = this.createMockFeedback();
-  this.runWithLoadedTree(
-      `
+TEST_F(
+    'ChromeVoxSmartStickyModeTest', 'SmartStickyModeEarcons', async function() {
+      const mockFeedback = this.createMockFeedback();
+      const root = await this.runWithLoadedTree(`
     <p>start</p>
     <input type="text"></input>
     <button>end</button>
-  `,
-      function(root) {
-        mockFeedback.call(doCmd('toggleStickyMode'))
-            .expectSpeech('Sticky mode enabled')
-            .call(doCmd('nextObject'))
-            .expectEarcon(Earcon.SMART_STICKY_MODE_OFF)
-            .expectSpeech('Sticky mode disabled')
-            .expectSpeech('Edit text')
-            .call(() => assertFalse(ChromeVox.isStickyModeOn()))
-            .call(doCmd('nextObject'))
-            .expectEarcon(Earcon.SMART_STICKY_MODE_ON)
-            .expectSpeech('Sticky mode enabled')
-            .expectSpeech('Button')
-            .call(() => assertTrue(ChromeVox.isStickyModeOn()))
-            .replay();
-      });
-});
+  `);
+      mockFeedback.call(doCmd('toggleStickyMode'))
+          .expectSpeech('Sticky mode enabled')
+          .call(doCmd('nextObject'))
+          .expectEarcon(Earcon.SMART_STICKY_MODE_OFF)
+          .expectSpeech('Sticky mode disabled')
+          .expectSpeech('Edit text')
+          .call(() => assertFalse(ChromeVox.isStickyModeOn()))
+          .call(doCmd('nextObject'))
+          .expectEarcon(Earcon.SMART_STICKY_MODE_ON)
+          .expectSpeech('Sticky mode enabled')
+          .expectSpeech('Button')
+          .call(() => assertTrue(ChromeVox.isStickyModeOn()))
+          .replay();
+    });
 
-TEST_F('ChromeVoxSmartStickyModeTest', 'ContinuousRead', function() {
+TEST_F('ChromeVoxSmartStickyModeTest', 'ContinuousRead', async function() {
   const mockFeedback = this.createMockFeedback();
   const site = `
     <p>start</p>
     <input type="text"></input>
     <button>end</button>
   `;
-  this.runWithLoadedTree(site, function(root) {
-    // Fake the read from here/continuous read state.
-    ChromeVoxState.isReadingContinuously = true;
-    mockFeedback.call(doCmd('toggleStickyMode'))
-        .expectSpeech('Sticky mode enabled')
-        .call(doCmd('nextObject'))
-        .expectNextSpeechUtteranceIsNot('Sticky mode disabled')
-        .expectSpeech('Edit text')
-        .call(() => assertTrue(ChromeVox.isStickyModeOn()))
-        .call(doCmd('nextObject'))
-        .expectNextSpeechUtteranceIsNot('Sticky mode enabled')
-        .expectSpeech('Button')
-        .call(() => assertTrue(ChromeVox.isStickyModeOn()))
-        .replay();
-  });
+  const root = await this.runWithLoadedTree(site);
+  // Fake the read from here/continuous read state.
+  ChromeVoxState.isReadingContinuously = true;
+  mockFeedback.call(doCmd('toggleStickyMode'))
+      .expectSpeech('Sticky mode enabled')
+      .call(doCmd('nextObject'))
+      .expectNextSpeechUtteranceIsNot('Sticky mode disabled')
+      .expectSpeech('Edit text')
+      .call(() => assertTrue(ChromeVox.isStickyModeOn()))
+      .call(doCmd('nextObject'))
+      .expectNextSpeechUtteranceIsNot('Sticky mode enabled')
+      .expectSpeech('Button')
+      .call(() => assertTrue(ChromeVox.isStickyModeOn()))
+      .replay();
 });
diff --git a/chrome/browser/resources/chromeos/accessibility/chromevox/background/user_action_monitor_test.js b/chrome/browser/resources/chromeos/accessibility/chromevox/background/user_action_monitor_test.js
index 9e2c262..2c10835 100644
--- a/chrome/browser/resources/chromeos/accessibility/chromevox/background/user_action_monitor_test.js
+++ b/chrome/browser/resources/chromeos/accessibility/chromevox/background/user_action_monitor_test.js
@@ -41,457 +41,447 @@
   }
 };
 
-TEST_F('ChromeVoxUserActionMonitorTest', 'UnitTest', function() {
-  this.runWithLoadedTree(this.simpleDoc, function() {
-    let finished = false;
-    const actions = [
-      {
-        type: 'key_sequence',
-        value: {'keys': {'keyCode': [KeyCode.SPACE]}},
-      },
-      {type: 'braille', value: 'jumpToTop'},
-      {type: 'gesture', value: Gesture.SWIPE_UP1}
-    ];
-    const onFinished = () => finished = true;
-
-    const monitor = new UserActionMonitor(actions, onFinished);
-    assertEquals(3, monitor.actions_.length);
-    assertEquals(0, monitor.actionIndex_);
-    assertEquals('key_sequence', monitor.getExpectedAction_().type);
-    assertFalse(finished);
-    monitor.expectedActionMatched_();
-    assertEquals(1, monitor.actionIndex_);
-    assertEquals('braille', monitor.getExpectedAction_().type);
-    assertFalse(finished);
-    monitor.expectedActionMatched_();
-    assertEquals(2, monitor.actionIndex_);
-    assertEquals('gesture', monitor.getExpectedAction_().type);
-    assertFalse(finished);
-    monitor.expectedActionMatched_();
-    assertTrue(finished);
-    assertEquals(3, monitor.actions_.length);
-    assertEquals(3, monitor.actionIndex_);
-  });
-});
-
-TEST_F('ChromeVoxUserActionMonitorTest', 'ActionUnitTest', function() {
-  this.runWithLoadedTree(this.simpleDoc, function() {
-    const keySequenceActionOne = UserActionMonitor.Action.fromActionInfo(
-        {type: 'key_sequence', value: {keys: {keyCode: [KeyCode.SPACE]}}});
-    const keySequenceActionTwo = new UserActionMonitor.Action({
+TEST_F('ChromeVoxUserActionMonitorTest', 'UnitTest', async function() {
+  await this.runWithLoadedTree(this.simpleDoc);
+  let finished = false;
+  const actions = [
+    {
       type: 'key_sequence',
-      value: new KeySequence(TestUtils.createMockKeyEvent(KeyCode.A))
-    });
-    const gestureActionOne = UserActionMonitor.Action.fromActionInfo(
-        {type: 'gesture', value: Gesture.SWIPE_UP1});
-    const gestureActionTwo = new UserActionMonitor.Action(
-        {type: 'gesture', value: Gesture.SWIPE_UP2});
+      value: {'keys': {'keyCode': [KeyCode.SPACE]}},
+    },
+    {type: 'braille', value: 'jumpToTop'},
+    {type: 'gesture', value: Gesture.SWIPE_UP1}
+  ];
+  const onFinished = () => finished = true;
 
-    assertFalse(keySequenceActionOne.equals(keySequenceActionTwo));
-    assertFalse(keySequenceActionOne.equals(gestureActionOne));
-    assertFalse(keySequenceActionOne.equals(gestureActionTwo));
-    assertFalse(keySequenceActionTwo.equals(gestureActionOne));
-    assertFalse(keySequenceActionTwo.equals(gestureActionTwo));
-    assertFalse(gestureActionOne.equals(gestureActionTwo));
-
-    const cloneKeySequenceActionOne = UserActionMonitor.Action.fromActionInfo(
-        {type: 'key_sequence', value: {keys: {keyCode: [KeyCode.SPACE]}}});
-    const cloneGestureActionOne = new UserActionMonitor.Action(
-        {type: 'gesture', value: Gesture.SWIPE_UP1});
-    assertTrue(keySequenceActionOne.equals(cloneKeySequenceActionOne));
-    assertTrue(gestureActionOne.equals(cloneGestureActionOne));
-  });
+  const monitor = new UserActionMonitor(actions, onFinished);
+  assertEquals(3, monitor.actions_.length);
+  assertEquals(0, monitor.actionIndex_);
+  assertEquals('key_sequence', monitor.getExpectedAction_().type);
+  assertFalse(finished);
+  monitor.expectedActionMatched_();
+  assertEquals(1, monitor.actionIndex_);
+  assertEquals('braille', monitor.getExpectedAction_().type);
+  assertFalse(finished);
+  monitor.expectedActionMatched_();
+  assertEquals(2, monitor.actionIndex_);
+  assertEquals('gesture', monitor.getExpectedAction_().type);
+  assertFalse(finished);
+  monitor.expectedActionMatched_();
+  assertTrue(finished);
+  assertEquals(3, monitor.actions_.length);
+  assertEquals(3, monitor.actionIndex_);
 });
 
-TEST_F('ChromeVoxUserActionMonitorTest', 'Errors', function() {
-  this.runWithLoadedTree(this.simpleDoc, function() {
-    let monitor;
-    let caught = false;
-    let finished = false;
-    const actions = [
-      {
-        type: 'key_sequence',
-        value: {'keys': {'keyCode': [KeyCode.SPACE]}},
-      },
-    ];
-    const onFinished = () => finished = true;
-    const assertCaughtAndReset = () => {
-      assertTrue(caught);
-      caught = false;
-    };
+TEST_F('ChromeVoxUserActionMonitorTest', 'ActionUnitTest', async function() {
+  await this.runWithLoadedTree(this.simpleDoc);
+  const keySequenceActionOne = UserActionMonitor.Action.fromActionInfo(
+      {type: 'key_sequence', value: {keys: {keyCode: [KeyCode.SPACE]}}});
+  const keySequenceActionTwo = new UserActionMonitor.Action({
+    type: 'key_sequence',
+    value: new KeySequence(TestUtils.createMockKeyEvent(KeyCode.A))
+  });
+  const gestureActionOne = UserActionMonitor.Action.fromActionInfo(
+      {type: 'gesture', value: Gesture.SWIPE_UP1});
+  const gestureActionTwo =
+      new UserActionMonitor.Action({type: 'gesture', value: Gesture.SWIPE_UP2});
 
-    try {
-      monitor = new UserActionMonitor([], onFinished);
-      assertTrue(false);  // Shouldn't execute.
-    } catch (error) {
-      assertEquals(
-          `UserActionMonitor: actionInfos can't be empty`, error.message);
-      caught = true;
-    }
-    assertCaughtAndReset();
-    try {
-      new UserActionMonitor.Action({type: 'key_sequence', value: 'invalid'});
-      assertTrue(false);  // Shouldn't execute
-    } catch (error) {
-      assertEquals(
-          'UserActionMonitor: Must provide a KeySequence value for Actions ' +
-              'of type ActionType.KEY_SEQUENCE',
-          error.message);
-      caught = true;
-    }
-    assertCaughtAndReset();
-    try {
-      UserActionMonitor.Action.fromActionInfo({type: 'gesture', value: false});
-      assertTrue(false);  // Shouldn't execute.
-    } catch (error) {
-      assertEquals(
-          'UserActionMonitor: Must provide a string value for Actions if ' +
-              'type is other than ActionType.KEY_SEQUENCE',
-          error.message);
-      caught = true;
-    }
-    assertCaughtAndReset();
+  assertFalse(keySequenceActionOne.equals(keySequenceActionTwo));
+  assertFalse(keySequenceActionOne.equals(gestureActionOne));
+  assertFalse(keySequenceActionOne.equals(gestureActionTwo));
+  assertFalse(keySequenceActionTwo.equals(gestureActionOne));
+  assertFalse(keySequenceActionTwo.equals(gestureActionTwo));
+  assertFalse(gestureActionOne.equals(gestureActionTwo));
 
-    monitor = new UserActionMonitor(actions, onFinished);
-    monitor.expectedActionMatched_();
-    assertTrue(finished);
+  const cloneKeySequenceActionOne = UserActionMonitor.Action.fromActionInfo(
+      {type: 'key_sequence', value: {keys: {keyCode: [KeyCode.SPACE]}}});
+  const cloneGestureActionOne =
+      new UserActionMonitor.Action({type: 'gesture', value: Gesture.SWIPE_UP1});
+  assertTrue(keySequenceActionOne.equals(cloneKeySequenceActionOne));
+  assertTrue(gestureActionOne.equals(cloneGestureActionOne));
+});
 
-    try {
-      monitor.onKeySequence(
-          new KeySequence(TestUtils.createMockKeyEvent(KeyCode.SPACE)));
-      assertTrue(false);  // Shouldn't execute.
-    } catch (error) {
-      assertEquals(
-          'UserActionMonitor: actionIndex_ is invalid.', error.message);
-      caught = true;
-    }
-    assertCaughtAndReset();
-    try {
-      monitor.expectedActionMatched_();
-      assertTrue(false);  // Shouldn't execute.
-    } catch (error) {
-      assertEquals(
-          'UserActionMonitor: actionIndex_ is invalid.', error.message);
-      caught = true;
-    }
-    assertCaughtAndReset();
-    try {
-      monitor.nextAction_();
-      assertTrue(false);  // Shouldn't execute.
-    } catch (error) {
-      assertEquals(
-          `UserActionMonitor: can't call nextAction_(), invalid index`,
-          error.message);
-      caught = true;
-    }
+TEST_F('ChromeVoxUserActionMonitorTest', 'Errors', async function() {
+  await this.runWithLoadedTree(this.simpleDoc);
+  let monitor;
+  let caught = false;
+  let finished = false;
+  const actions = [
+    {
+      type: 'key_sequence',
+      value: {'keys': {'keyCode': [KeyCode.SPACE]}},
+    },
+  ];
+  const onFinished = () => finished = true;
+  const assertCaughtAndReset = () => {
     assertTrue(caught);
-  });
+    caught = false;
+  };
+
+  try {
+    monitor = new UserActionMonitor([], onFinished);
+    assertTrue(false);  // Shouldn't execute.
+  } catch (error) {
+    assertEquals(
+        `UserActionMonitor: actionInfos can't be empty`, error.message);
+    caught = true;
+  }
+  assertCaughtAndReset();
+  try {
+    new UserActionMonitor.Action({type: 'key_sequence', value: 'invalid'});
+    assertTrue(false);  // Shouldn't execute
+  } catch (error) {
+    assertEquals(
+        'UserActionMonitor: Must provide a KeySequence value for Actions ' +
+            'of type ActionType.KEY_SEQUENCE',
+        error.message);
+    caught = true;
+  }
+  assertCaughtAndReset();
+  try {
+    UserActionMonitor.Action.fromActionInfo({type: 'gesture', value: false});
+    assertTrue(false);  // Shouldn't execute.
+  } catch (error) {
+    assertEquals(
+        'UserActionMonitor: Must provide a string value for Actions if ' +
+            'type is other than ActionType.KEY_SEQUENCE',
+        error.message);
+    caught = true;
+  }
+  assertCaughtAndReset();
+
+  monitor = new UserActionMonitor(actions, onFinished);
+  monitor.expectedActionMatched_();
+  assertTrue(finished);
+
+  try {
+    monitor.onKeySequence(
+        new KeySequence(TestUtils.createMockKeyEvent(KeyCode.SPACE)));
+    assertTrue(false);  // Shouldn't execute.
+  } catch (error) {
+    assertEquals('UserActionMonitor: actionIndex_ is invalid.', error.message);
+    caught = true;
+  }
+  assertCaughtAndReset();
+  try {
+    monitor.expectedActionMatched_();
+    assertTrue(false);  // Shouldn't execute.
+  } catch (error) {
+    assertEquals('UserActionMonitor: actionIndex_ is invalid.', error.message);
+    caught = true;
+  }
+  assertCaughtAndReset();
+  try {
+    monitor.nextAction_();
+    assertTrue(false);  // Shouldn't execute.
+  } catch (error) {
+    assertEquals(
+        `UserActionMonitor: can't call nextAction_(), invalid index`,
+        error.message);
+    caught = true;
+  }
+  assertTrue(caught);
 });
 
-TEST_F('ChromeVoxUserActionMonitorTest', 'Output', function() {
+TEST_F('ChromeVoxUserActionMonitorTest', 'Output', async function() {
   const mockFeedback = this.createMockFeedback();
-  this.runWithLoadedTree(this.simpleDoc, function(rootNode) {
-    let monitor;
-    let finished = false;
-    const actions = [
-      {
-        type: 'gesture',
-        value: Gesture.SWIPE_UP1,
-        beforeActionMsg: 'First instruction',
-        afterActionMsg: 'Congratulations!'
-      },
-      {
-        type: 'gesture',
-        value: Gesture.SWIPE_UP1,
-        beforeActionMsg: 'Second instruction',
-        afterActionMsg: 'You did it!'
-      }
-    ];
-    const onFinished = () => finished = true;
+  const rootNode = await this.runWithLoadedTree(this.simpleDoc);
+  let monitor;
+  let finished = false;
+  const actions = [
+    {
+      type: 'gesture',
+      value: Gesture.SWIPE_UP1,
+      beforeActionMsg: 'First instruction',
+      afterActionMsg: 'Congratulations!'
+    },
+    {
+      type: 'gesture',
+      value: Gesture.SWIPE_UP1,
+      beforeActionMsg: 'Second instruction',
+      afterActionMsg: 'You did it!'
+    }
+  ];
+  const onFinished = () => finished = true;
 
-    mockFeedback
-        .call(() => {
-          monitor = new UserActionMonitor(actions, onFinished);
-        })
-        .expectSpeech('First instruction')
-        .call(() => {
-          monitor.expectedActionMatched_();
-          assertFalse(finished);
-        })
-        .expectSpeech('Congratulations!', 'Second instruction')
-        .call(() => {
-          monitor.expectedActionMatched_();
-          assertTrue(finished);
-        })
-        .expectSpeech('You did it!');
-    mockFeedback.replay();
-  });
+  mockFeedback
+      .call(() => {
+        monitor = new UserActionMonitor(actions, onFinished);
+      })
+      .expectSpeech('First instruction')
+      .call(() => {
+        monitor.expectedActionMatched_();
+        assertFalse(finished);
+      })
+      .expectSpeech('Congratulations!', 'Second instruction')
+      .call(() => {
+        monitor.expectedActionMatched_();
+        assertTrue(finished);
+      })
+      .expectSpeech('You did it!');
+  mockFeedback.replay();
 });
 
 // Tests that we can match a single key. Serves as an integration test
 // since we don't directly call a UserActionMonitor function.
-TEST_F('ChromeVoxUserActionMonitorTest', 'SingleKey', function() {
-  this.runWithLoadedTree(this.simpleDoc, function() {
-    const keyboardHandler = new BackgroundKeyboardHandler();
-    let finished = false;
-    const actions =
-        [{type: 'key_sequence', value: {'keys': {'keyCode': [KeyCode.SPACE]}}}];
-    const onFinished = () => finished = true;
+TEST_F('ChromeVoxUserActionMonitorTest', 'SingleKey', async function() {
+  await this.runWithLoadedTree(this.simpleDoc);
+  const keyboardHandler = new BackgroundKeyboardHandler();
+  let finished = false;
+  const actions =
+      [{type: 'key_sequence', value: {'keys': {'keyCode': [KeyCode.SPACE]}}}];
+  const onFinished = () => finished = true;
 
-    ChromeVoxState.instance.createUserActionMonitor(actions, onFinished);
-    keyboardHandler.onKeyDown(TestUtils.createMockKeyEvent(KeyCode.LEFT));
-    keyboardHandler.onKeyUp(TestUtils.createMockKeyEvent(KeyCode.LEFT));
-    assertFalse(finished);
-    keyboardHandler.onKeyDown(TestUtils.createMockKeyEvent(KeyCode.RIGHT));
-    keyboardHandler.onKeyUp(TestUtils.createMockKeyEvent(KeyCode.RIGHT));
-    assertFalse(finished);
-    keyboardHandler.onKeyDown(TestUtils.createMockKeyEvent(KeyCode.SPACE));
-    keyboardHandler.onKeyUp(TestUtils.createMockKeyEvent(KeyCode.SPACE));
-    assertTrue(finished);
-  });
+  ChromeVoxState.instance.createUserActionMonitor(actions, onFinished);
+  keyboardHandler.onKeyDown(TestUtils.createMockKeyEvent(KeyCode.LEFT));
+  keyboardHandler.onKeyUp(TestUtils.createMockKeyEvent(KeyCode.LEFT));
+  assertFalse(finished);
+  keyboardHandler.onKeyDown(TestUtils.createMockKeyEvent(KeyCode.RIGHT));
+  keyboardHandler.onKeyUp(TestUtils.createMockKeyEvent(KeyCode.RIGHT));
+  assertFalse(finished);
+  keyboardHandler.onKeyDown(TestUtils.createMockKeyEvent(KeyCode.SPACE));
+  keyboardHandler.onKeyUp(TestUtils.createMockKeyEvent(KeyCode.SPACE));
+  assertTrue(finished);
 });
 
 // Tests that we can match a key sequence. Serves as an integration test
 // since we don't directly call a UserActionMonitor function.
-TEST_F('ChromeVoxUserActionMonitorTest', 'MultipleKeys', function() {
-  this.runWithLoadedTree(this.simpleDoc, function() {
-    const keyboardHandler = new BackgroundKeyboardHandler();
-    let finished = false;
-    const actions = [{
-      type: 'key_sequence',
-      value: {'cvoxModifier': true, 'keys': {'keyCode': [KeyCode.O, KeyCode.B]}}
-    }];
-    const onFinished = () => finished = true;
+TEST_F('ChromeVoxUserActionMonitorTest', 'MultipleKeys', async function() {
+  await this.runWithLoadedTree(this.simpleDoc);
+  const keyboardHandler = new BackgroundKeyboardHandler();
+  let finished = false;
+  const actions = [{
+    type: 'key_sequence',
+    value: {'cvoxModifier': true, 'keys': {'keyCode': [KeyCode.O, KeyCode.B]}}
+  }];
+  const onFinished = () => finished = true;
 
-    ChromeVoxState.instance.createUserActionMonitor(actions, onFinished);
-    keyboardHandler.onKeyDown(TestUtils.createMockKeyEvent(KeyCode.O));
-    keyboardHandler.onKeyUp(TestUtils.createMockKeyEvent(KeyCode.O));
-    assertFalse(finished);
-    keyboardHandler.onKeyDown(TestUtils.createMockKeyEvent(KeyCode.B));
-    keyboardHandler.onKeyUp(TestUtils.createMockKeyEvent(KeyCode.B));
-    assertFalse(finished);
-    keyboardHandler.onKeyDown(TestUtils.createMockKeyEvent(KeyCode.SEARCH));
-    keyboardHandler.onKeyUp(TestUtils.createMockKeyEvent(KeyCode.SEARCH));
-    assertFalse(finished);
-    keyboardHandler.onKeyDown(
-        TestUtils.createMockKeyEvent(KeyCode.O, {searchKeyHeld: true}));
-    assertFalse(finished);
-    keyboardHandler.onKeyUp(
-        TestUtils.createMockKeyEvent(KeyCode.O, {searchKeyHeld: true}));
-    keyboardHandler.onKeyDown(TestUtils.createMockKeyEvent(KeyCode.B));
-    assertTrue(finished);
-  });
+  ChromeVoxState.instance.createUserActionMonitor(actions, onFinished);
+  keyboardHandler.onKeyDown(TestUtils.createMockKeyEvent(KeyCode.O));
+  keyboardHandler.onKeyUp(TestUtils.createMockKeyEvent(KeyCode.O));
+  assertFalse(finished);
+  keyboardHandler.onKeyDown(TestUtils.createMockKeyEvent(KeyCode.B));
+  keyboardHandler.onKeyUp(TestUtils.createMockKeyEvent(KeyCode.B));
+  assertFalse(finished);
+  keyboardHandler.onKeyDown(TestUtils.createMockKeyEvent(KeyCode.SEARCH));
+  keyboardHandler.onKeyUp(TestUtils.createMockKeyEvent(KeyCode.SEARCH));
+  assertFalse(finished);
+  keyboardHandler.onKeyDown(
+      TestUtils.createMockKeyEvent(KeyCode.O, {searchKeyHeld: true}));
+  assertFalse(finished);
+  keyboardHandler.onKeyUp(
+      TestUtils.createMockKeyEvent(KeyCode.O, {searchKeyHeld: true}));
+  keyboardHandler.onKeyDown(TestUtils.createMockKeyEvent(KeyCode.B));
+  assertTrue(finished);
 });
 
 // Tests that we can match multiple key sequences.
-TEST_F('ChromeVoxUserActionMonitorTest', 'MultipleKeySequences', function() {
-  const mockFeedback = this.createMockFeedback();
-  this.runWithLoadedTree(this.simpleDoc, function() {
-    let finished = false;
-    const actions = [
-      {
-        type: 'key_sequence',
-        value: {
-          'keys': {'altKey': [true], 'shiftKey': [true], 'keyCode': [KeyCode.L]}
+TEST_F(
+    'ChromeVoxUserActionMonitorTest', 'MultipleKeySequences', async function() {
+      const mockFeedback = this.createMockFeedback();
+      await this.runWithLoadedTree(this.simpleDoc);
+      let finished = false;
+      const actions = [
+        {
+          type: 'key_sequence',
+          value: {
+            'keys':
+                {'altKey': [true], 'shiftKey': [true], 'keyCode': [KeyCode.L]}
+          },
+          afterActionMsg: 'You pressed the first sequence!'
         },
-        afterActionMsg: 'You pressed the first sequence!'
-      },
-      {
-        type: 'key_sequence',
-        value: {
-          'keys': {'altKey': [true], 'shiftKey': [true], 'keyCode': [KeyCode.S]}
-        },
-        afterActionMsg: 'You pressed the second sequence!'
-      }
-    ];
-    const onFinished = () => finished = true;
+        {
+          type: 'key_sequence',
+          value: {
+            'keys':
+                {'altKey': [true], 'shiftKey': [true], 'keyCode': [KeyCode.S]}
+          },
+          afterActionMsg: 'You pressed the second sequence!'
+        }
+      ];
+      const onFinished = () => finished = true;
 
-    const altShiftLSequence = new KeySequence(TestUtils.createMockKeyEvent(
-        KeyCode.L, {altKey: true, shiftKey: true}));
-    const altShiftSSequence = new KeySequence(TestUtils.createMockKeyEvent(
-        KeyCode.S, {altKey: true, shiftKey: true}));
-    let monitor;
-    mockFeedback
-        .call(() => {
-          monitor = new UserActionMonitor(actions, onFinished);
-          assertFalse(monitor.onKeySequence(altShiftSSequence));
-          assertFalse(finished);
-          assertTrue(monitor.onKeySequence(altShiftLSequence));
-          assertFalse(finished);
-        })
-        .expectSpeech('You pressed the first sequence!')
-        .call(() => {
-          assertFalse(monitor.onKeySequence(altShiftLSequence));
-          assertFalse(finished);
-          assertTrue(monitor.onKeySequence(altShiftSSequence));
-          assertTrue(finished);
-        })
-        .expectSpeech('You pressed the second sequence!');
-    mockFeedback.replay();
-  });
-});
+      const altShiftLSequence = new KeySequence(TestUtils.createMockKeyEvent(
+          KeyCode.L, {altKey: true, shiftKey: true}));
+      const altShiftSSequence = new KeySequence(TestUtils.createMockKeyEvent(
+          KeyCode.S, {altKey: true, shiftKey: true}));
+      let monitor;
+      mockFeedback
+          .call(() => {
+            monitor = new UserActionMonitor(actions, onFinished);
+            assertFalse(monitor.onKeySequence(altShiftSSequence));
+            assertFalse(finished);
+            assertTrue(monitor.onKeySequence(altShiftLSequence));
+            assertFalse(finished);
+          })
+          .expectSpeech('You pressed the first sequence!')
+          .call(() => {
+            assertFalse(monitor.onKeySequence(altShiftLSequence));
+            assertFalse(finished);
+            assertTrue(monitor.onKeySequence(altShiftSSequence));
+            assertTrue(finished);
+          })
+          .expectSpeech('You pressed the second sequence!');
+      mockFeedback.replay();
+    });
 
 // Tests that we can provide expectations for ChromeVox commands and block
 // command execution until the desired command is performed. Serves as an
 // integration test since we don't directly call a UserActionMonitor function.
-TEST_F('ChromeVoxUserActionMonitorTest', 'BlockCommands', function() {
+TEST_F('ChromeVoxUserActionMonitorTest', 'BlockCommands', async function() {
   const mockFeedback = this.createMockFeedback();
-  this.runWithLoadedTree(this.paragraphDoc, function() {
-    const keyboardHandler = new BackgroundKeyboardHandler();
-    let finished = false;
-    const actions = [
-      {
-        type: 'key_sequence',
-        value: {'cvoxModifier': true, 'keys': {'keyCode': [KeyCode.RIGHT]}}
-      },
-      {
-        type: 'key_sequence',
-        value: {'cvoxModifier': true, 'keys': {'keyCode': [KeyCode.LEFT]}}
-      }
-    ];
-    const onFinished = () => finished = true;
+  await this.runWithLoadedTree(this.paragraphDoc);
+  const keyboardHandler = new BackgroundKeyboardHandler();
+  let finished = false;
+  const actions = [
+    {
+      type: 'key_sequence',
+      value: {'cvoxModifier': true, 'keys': {'keyCode': [KeyCode.RIGHT]}}
+    },
+    {
+      type: 'key_sequence',
+      value: {'cvoxModifier': true, 'keys': {'keyCode': [KeyCode.LEFT]}}
+    }
+  ];
+  const onFinished = () => finished = true;
 
-    const nextObject =
-        TestUtils.createMockKeyEvent(KeyCode.RIGHT, {searchKeyHeld: true});
-    const nextLine =
-        TestUtils.createMockKeyEvent(KeyCode.DOWN, {searchKeyHeld: true});
-    const previousObject =
-        TestUtils.createMockKeyEvent(KeyCode.LEFT, {searchKeyHeld: true});
-    const previousLine =
-        TestUtils.createMockKeyEvent(KeyCode.UP, {searchKeyHeld: true});
+  const nextObject =
+      TestUtils.createMockKeyEvent(KeyCode.RIGHT, {searchKeyHeld: true});
+  const nextLine =
+      TestUtils.createMockKeyEvent(KeyCode.DOWN, {searchKeyHeld: true});
+  const previousObject =
+      TestUtils.createMockKeyEvent(KeyCode.LEFT, {searchKeyHeld: true});
+  const previousLine =
+      TestUtils.createMockKeyEvent(KeyCode.UP, {searchKeyHeld: true});
 
-    ChromeVoxState.instance.createUserActionMonitor(actions, onFinished);
-    mockFeedback.expectSpeech('Start')
-        .call(() => {
-          assertEquals('Start', this.getRangeStart().name);
-        })
-        .call(() => {
-          // Calling nextLine doesn't move ChromeVox because UserActionMonitor
-          // expects the nextObject command.
-          keyboardHandler.onKeyDown(nextLine);
-          keyboardHandler.onKeyUp(nextLine);
-          assertEquals('Start', this.getRangeStart().name);
-        })
-        .call(() => {
-          keyboardHandler.onKeyDown(nextObject);
-          keyboardHandler.onKeyUp(nextObject);
-          assertEquals('End', this.getRangeStart().name);
-        })
-        .expectSpeech('End')
-        .call(() => {
-          // Calling previousLine doesn't move ChromeVox because
-          // UserActionMonitor expects the previousObject command.
-          keyboardHandler.onKeyDown(previousLine);
-          keyboardHandler.onKeyUp(previousLine);
-          assertEquals('End', this.getRangeStart().name);
-        })
-        .call(() => {
-          keyboardHandler.onKeyDown(previousObject);
-          keyboardHandler.onKeyUp(previousObject);
-          assertEquals('Start', this.getRangeStart().name);
-        })
-        .expectSpeech('Start')
-        .replay();
-  });
+  ChromeVoxState.instance.createUserActionMonitor(actions, onFinished);
+  mockFeedback.expectSpeech('Start')
+      .call(() => {
+        assertEquals('Start', this.getRangeStart().name);
+      })
+      .call(() => {
+        // Calling nextLine doesn't move ChromeVox because UserActionMonitor
+        // expects the nextObject command.
+        keyboardHandler.onKeyDown(nextLine);
+        keyboardHandler.onKeyUp(nextLine);
+        assertEquals('Start', this.getRangeStart().name);
+      })
+      .call(() => {
+        keyboardHandler.onKeyDown(nextObject);
+        keyboardHandler.onKeyUp(nextObject);
+        assertEquals('End', this.getRangeStart().name);
+      })
+      .expectSpeech('End')
+      .call(() => {
+        // Calling previousLine doesn't move ChromeVox because
+        // UserActionMonitor expects the previousObject command.
+        keyboardHandler.onKeyDown(previousLine);
+        keyboardHandler.onKeyUp(previousLine);
+        assertEquals('End', this.getRangeStart().name);
+      })
+      .call(() => {
+        keyboardHandler.onKeyDown(previousObject);
+        keyboardHandler.onKeyUp(previousObject);
+        assertEquals('Start', this.getRangeStart().name);
+      })
+      .expectSpeech('Start')
+      .replay();
 });
 
 // Tests that a user can close ChromeVox (Ctrl + Alt + Z) when UserActionMonitor
 // is active.
-TEST_F('ChromeVoxUserActionMonitorTest', 'CloseChromeVox', function() {
-  this.runWithLoadedTree(this.simpleDoc, function() {
-    const keyboardHandler = new BackgroundKeyboardHandler();
-    let finished = false;
-    let closed = false;
-    const actions =
-        [{type: 'key_sequence', value: {'keys': {'keyCode': [KeyCode.A]}}}];
-    const onFinished = () => finished = true;
-    ChromeVoxState.instance.createUserActionMonitor(actions, onFinished);
-    // Swap in the below function so we don't actually close ChromeVox.
-    UserActionMonitor.closeChromeVox_ = () => {
-      closed = true;
-    };
+TEST_F('ChromeVoxUserActionMonitorTest', 'CloseChromeVox', async function() {
+  await this.runWithLoadedTree(this.simpleDoc);
+  const keyboardHandler = new BackgroundKeyboardHandler();
+  let finished = false;
+  let closed = false;
+  const actions =
+      [{type: 'key_sequence', value: {'keys': {'keyCode': [KeyCode.A]}}}];
+  const onFinished = () => finished = true;
+  ChromeVoxState.instance.createUserActionMonitor(actions, onFinished);
+  // Swap in the below function so we don't actually close ChromeVox.
+  UserActionMonitor.closeChromeVox_ = () => {
+    closed = true;
+  };
 
-    assertFalse(closed);
-    assertFalse(finished);
-    keyboardHandler.onKeyDown(
-        TestUtils.createMockKeyEvent(KeyCode.CONTROL, {ctrlKey: true}));
-    assertFalse(closed);
-    assertFalse(finished);
-    keyboardHandler.onKeyDown(TestUtils.createMockKeyEvent(
-        KeyCode.ALT, {ctrlKey: true, altKey: true}));
-    assertFalse(closed);
-    assertFalse(finished);
-    keyboardHandler.onKeyDown(
-        TestUtils.createMockKeyEvent(KeyCode.Z, {ctrlKey: true, altKey: true}));
-    assertTrue(closed);
-    // |finished| remains false since we didn't press the expected key sequence.
-    assertFalse(finished);
-  });
+  assertFalse(closed);
+  assertFalse(finished);
+  keyboardHandler.onKeyDown(
+      TestUtils.createMockKeyEvent(KeyCode.CONTROL, {ctrlKey: true}));
+  assertFalse(closed);
+  assertFalse(finished);
+  keyboardHandler.onKeyDown(
+      TestUtils.createMockKeyEvent(KeyCode.ALT, {ctrlKey: true, altKey: true}));
+  assertFalse(closed);
+  assertFalse(finished);
+  keyboardHandler.onKeyDown(
+      TestUtils.createMockKeyEvent(KeyCode.Z, {ctrlKey: true, altKey: true}));
+  assertTrue(closed);
+  // |finished| remains false since we didn't press the expected key sequence.
+  assertFalse(finished);
 });
 
 // Tests that we can stop propagation of an action, even if it is matched.
 // In this test, we stop propagation of the Control key to avoid executing the
 // stopSpeech command.
-TEST_F('ChromeVoxUserActionMonitorTest', 'StopPropagation', function() {
-  this.runWithLoadedTree(this.simpleDoc, function() {
-    const keyboardHandler = ChromeVoxState.instance.keyboardHandler_;
-    let finished = false;
-    let executedCommand = false;
-    const actions = [{
-      type: 'key_sequence',
-      value: {keys: {keyCode: [KeyCode.CONTROL]}},
-      shouldPropagate: false
-    }];
-    const onFinished = () => finished = true;
-    ChromeVoxState.instance.createUserActionMonitor(actions, onFinished);
-    ChromeVoxKbHandler.commandHandler = function(command) {
-      executedCommand = true;
-    };
-    assertFalse(finished);
-    assertFalse(executedCommand);
-    keyboardHandler.onKeyDown(TestUtils.createMockKeyEvent(KeyCode.CONTROL));
-    keyboardHandler.onKeyUp(TestUtils.createMockKeyEvent(KeyCode.CONTROL));
-    assertFalse(executedCommand);
-    assertTrue(finished);
-  });
+TEST_F('ChromeVoxUserActionMonitorTest', 'StopPropagation', async function() {
+  await this.runWithLoadedTree(this.simpleDoc);
+  const keyboardHandler = ChromeVoxState.instance.keyboardHandler_;
+  let finished = false;
+  let executedCommand = false;
+  const actions = [{
+    type: 'key_sequence',
+    value: {keys: {keyCode: [KeyCode.CONTROL]}},
+    shouldPropagate: false
+  }];
+  const onFinished = () => finished = true;
+  ChromeVoxState.instance.createUserActionMonitor(actions, onFinished);
+  ChromeVoxKbHandler.commandHandler = function(command) {
+    executedCommand = true;
+  };
+  assertFalse(finished);
+  assertFalse(executedCommand);
+  keyboardHandler.onKeyDown(TestUtils.createMockKeyEvent(KeyCode.CONTROL));
+  keyboardHandler.onKeyUp(TestUtils.createMockKeyEvent(KeyCode.CONTROL));
+  assertFalse(executedCommand);
+  assertTrue(finished);
 });
 
 // Tests that we can match a gesture when it's performed.
-TEST_F('ChromeVoxUserActionMonitorTest', 'Gestures', function() {
-  this.runWithLoadedTree(this.simpleDoc, function() {
-    let finished = false;
-    const actions = [{type: 'gesture', value: Gesture.SWIPE_RIGHT1}];
-    const onFinished = () => finished = true;
+TEST_F('ChromeVoxUserActionMonitorTest', 'Gestures', async function() {
+  await this.runWithLoadedTree(this.simpleDoc);
+  let finished = false;
+  const actions = [{type: 'gesture', value: Gesture.SWIPE_RIGHT1}];
+  const onFinished = () => finished = true;
 
-    ChromeVoxState.instance.createUserActionMonitor(actions, onFinished);
-    doGesture(Gesture.SWIPE_LEFT1)();
-    assertFalse(finished);
-    doGesture(Gesture.SWIPE_LEFT2)();
-    assertFalse(finished);
-    doGesture(Gesture.SWIPE_RIGHT1)();
-    assertTrue(finished);
-  });
+  ChromeVoxState.instance.createUserActionMonitor(actions, onFinished);
+  doGesture(Gesture.SWIPE_LEFT1)();
+  assertFalse(finished);
+  doGesture(Gesture.SWIPE_LEFT2)();
+  assertFalse(finished);
+  doGesture(Gesture.SWIPE_RIGHT1)();
+  assertTrue(finished);
 });
 
 // Tests that we can perform a command when an action has been matched.
-TEST_F('ChromeVoxUserActionMonitorTest', 'AfterActionCommand', function() {
-  const mockFeedback = this.createMockFeedback();
-  this.runWithLoadedTree(this.simpleDoc, function() {
-    let finished = false;
-    const actions = [{
-      type: 'gesture',
-      value: Gesture.SWIPE_RIGHT1,
-      afterActionCmd: 'announceBatteryDescription'
-    }];
-    const onFinished = () => finished = true;
+TEST_F(
+    'ChromeVoxUserActionMonitorTest', 'AfterActionCommand', async function() {
+      const mockFeedback = this.createMockFeedback();
+      await this.runWithLoadedTree(this.simpleDoc);
+      let finished = false;
+      const actions = [{
+        type: 'gesture',
+        value: Gesture.SWIPE_RIGHT1,
+        afterActionCmd: 'announceBatteryDescription'
+      }];
+      const onFinished = () => finished = true;
 
-    ChromeVoxState.instance.createUserActionMonitor(actions, onFinished);
-    mockFeedback
-        .call(() => {
-          doGesture(Gesture.SWIPE_RIGHT1)();
-          assertTrue(finished);
-        })
-        .expectSpeech(/Battery at [0-9]+ percent/)
-        .replay();
-  });
-});
+      ChromeVoxState.instance.createUserActionMonitor(actions, onFinished);
+      mockFeedback
+          .call(() => {
+            doGesture(Gesture.SWIPE_RIGHT1)();
+            assertTrue(finished);
+          })
+          .expectSpeech(/Battery at [0-9]+ percent/)
+          .replay();
+    });
diff --git a/chrome/browser/resources/chromeos/accessibility/chromevox/common/abstract_tts.js b/chrome/browser/resources/chromeos/accessibility/chromevox/common/abstract_tts.js
index fed89f4..1e19dd5 100644
--- a/chrome/browser/resources/chromeos/accessibility/chromevox/common/abstract_tts.js
+++ b/chrome/browser/resources/chromeos/accessibility/chromevox/common/abstract_tts.js
@@ -8,17 +8,11 @@
  *
  */
 
-goog.provide('AbstractTts');
-
-goog.require('Msgs');
-goog.require('TtsInterface');
-goog.require('goog.i18n.MessageFormat');
-
 /**
  * Creates a new instance.
  * @implements {TtsInterface}
  */
-AbstractTts = class {
+export class AbstractTts {
   constructor() {
     this.ttsProperties = new Object();
 
@@ -72,7 +66,12 @@
     }
   }
 
-  /** @override */
+  /**
+   * @param {string} textString
+   * @param {QueueMode} queueMode
+   * @param {Object=} properties
+   * @override
+   */
   speak(textString, queueMode, properties) {
     return this;
   }
@@ -274,7 +273,7 @@
       this.ttsProperties[key] = value;
     }
   }
-};
+}
 
 
 /**
diff --git a/chrome/browser/resources/chromeos/accessibility/chromevox/common/command_store.js b/chrome/browser/resources/chromeos/accessibility/chromevox/common/command_store.js
index 130ccf1..29d96864 100644
--- a/chrome/browser/resources/chromeos/accessibility/chromevox/common/command_store.js
+++ b/chrome/browser/resources/chromeos/accessibility/chromevox/common/command_store.js
@@ -30,8 +30,7 @@
  * categories.
  */
 
-
-goog.provide('CommandStore');
+export const CommandStore = {};
 
 /**
  * Returns all of the categories in the store as an array.
diff --git a/chrome/browser/resources/chromeos/accessibility/chromevox/common/console_tts.js b/chrome/browser/resources/chromeos/accessibility/chromevox/common/console_tts.js
index ffca66f..90621fe 100644
--- a/chrome/browser/resources/chromeos/accessibility/chromevox/common/console_tts.js
+++ b/chrome/browser/resources/chromeos/accessibility/chromevox/common/console_tts.js
@@ -10,7 +10,6 @@
 
 goog.require('LogStore');
 goog.require('SpeechLog');
-goog.require('AbstractTts');
 goog.require('TtsInterface');
 
 /**
diff --git a/chrome/browser/resources/chromeos/accessibility/chromevox/common/editable_text_base.js b/chrome/browser/resources/chromeos/accessibility/chromevox/common/editable_text_base.js
index cbd4b34..940acfd 100644
--- a/chrome/browser/resources/chromeos/accessibility/chromevox/common/editable_text_base.js
+++ b/chrome/browser/resources/chromeos/accessibility/chromevox/common/editable_text_base.js
@@ -15,6 +15,8 @@
  */
 import {ChromeVoxEvent} from '../background/custom_automation_event.js';
 
+import {AbstractTts} from './abstract_tts.js';
+
 /**
  * A class containing the information needed to speak
  * a text change event to the user.
diff --git a/chrome/browser/resources/chromeos/accessibility/chromevox/common/editable_text_base_test.js b/chrome/browser/resources/chromeos/accessibility/chromevox/common/editable_text_base_test.js
index 5e69248d..7a43e934 100644
--- a/chrome/browser/resources/chromeos/accessibility/chromevox/common/editable_text_base_test.js
+++ b/chrome/browser/resources/chromeos/accessibility/chromevox/common/editable_text_base_test.js
@@ -96,7 +96,14 @@
     })();
   }
 
+  /** @override */
   async setUpDeferred() {
+    await super.setUpDeferred();
+    await importModules('AbstractTts', '/chromevox/common/abstract_tts.js');
+    await importModules(
+        ['ChromeVoxEditableTextBase', 'TextChangedEvent', 'TypingEcho'],
+        '/chromevox/common/editable_text_base.js');
+
     // TODO: These tests are all assuming we used the IBeam cursor.
     // We need to add coverage for block cursor.
     ChromeVoxEditableTextBase.useIBeamCursor = true;
@@ -122,7 +129,6 @@
 ChromeVoxEditableTextUnitTest.prototype.extraLibraries = [
   '../../common/testing/assert_additions.js',
   '../../common/closure_shim.js',
-  'abstract_tts.js',
   'chromevox.js',
   'msgs.js',
   'tts_interface.js',
diff --git a/chrome/browser/resources/chromeos/accessibility/chromevox/common/keyboard_handler.js b/chrome/browser/resources/chromeos/accessibility/chromevox/common/keyboard_handler.js
index 431bc4b8..6f5ae18 100644
--- a/chrome/browser/resources/chromeos/accessibility/chromevox/common/keyboard_handler.js
+++ b/chrome/browser/resources/chromeos/accessibility/chromevox/common/keyboard_handler.js
@@ -5,6 +5,8 @@
 /**
  * @fileoverview Handles user keyboard input events.
  */
+import {KeyMap} from '../background/keymaps/key_map.js';
+
 export const ChromeVoxKbHandler = {};
 
 /**
diff --git a/chrome/browser/resources/chromeos/accessibility/chromevox/common/tts_background.js b/chrome/browser/resources/chromeos/accessibility/chromevox/common/tts_background.js
index b4ef6887..359e07a 100644
--- a/chrome/browser/resources/chromeos/accessibility/chromevox/common/tts_background.js
+++ b/chrome/browser/resources/chromeos/accessibility/chromevox/common/tts_background.js
@@ -7,6 +7,7 @@
  * extension API.
  */
 
+import {AbstractTts} from './abstract_tts.js';
 import {ChromeTtsBase} from './tts_base.js';
 
 const Utterance = class {
diff --git a/chrome/browser/resources/chromeos/accessibility/chromevox/common/tts_base.js b/chrome/browser/resources/chromeos/accessibility/chromevox/common/tts_base.js
index 1104df3c..31b5f04 100644
--- a/chrome/browser/resources/chromeos/accessibility/chromevox/common/tts_base.js
+++ b/chrome/browser/resources/chromeos/accessibility/chromevox/common/tts_base.js
@@ -6,6 +6,7 @@
  * @fileoverview A base class for Tts living on Chrome platforms.
  *
  */
+import {AbstractTts} from './abstract_tts.js';
 
 export class ChromeTtsBase extends AbstractTts {
   constructor() {
diff --git a/chrome/browser/resources/chromeos/accessibility/chromevox/learn_mode/kbexplorer_loader.js b/chrome/browser/resources/chromeos/accessibility/chromevox/learn_mode/kbexplorer_loader.js
index 4089af5..db7ddb8b 100644
--- a/chrome/browser/resources/chromeos/accessibility/chromevox/learn_mode/kbexplorer_loader.js
+++ b/chrome/browser/resources/chromeos/accessibility/chromevox/learn_mode/kbexplorer_loader.js
@@ -7,16 +7,13 @@
  */
 
 goog.require('BrailleCommandData');
-goog.require('BrailleKeyEvent');
-goog.require('Spannable');
-goog.require('AbstractTts');
 goog.require('BrailleKeyCommand');
+goog.require('BrailleKeyEvent');
 goog.require('ChromeVox');
 goog.require('ChromeVoxState');
-goog.require('CommandStore');
-goog.require('KeyMap');
 goog.require('KeySequence');
 goog.require('KeyUtil');
 goog.require('LibLouis');
 goog.require('Msgs');
 goog.require('NavBraille');
+goog.require('Spannable');
diff --git a/chrome/browser/resources/chromeos/accessibility/chromevox/learn_mode/learn_mode.js b/chrome/browser/resources/chromeos/accessibility/chromevox/learn_mode/learn_mode.js
index 8cd5e9f..673b3360 100644
--- a/chrome/browser/resources/chromeos/accessibility/chromevox/learn_mode/learn_mode.js
+++ b/chrome/browser/resources/chromeos/accessibility/chromevox/learn_mode/learn_mode.js
@@ -7,14 +7,14 @@
  *
  */
 import {GestureCommandData} from '../background/gesture_command_data.js';
+import {KeyMap} from '../background/keymaps/key_map.js';
+import {CommandStore} from '../common/command_store.js';
 import {ChromeVoxKbHandler} from '../common/keyboard_handler.js';
 
 /**
  * Class to manage the keyboard explorer.
  */
 export class LearnMode {
-  constructor() {}
-
   /**
    * Initialize keyboard explorer.
    */
diff --git a/chrome/browser/resources/chromeos/accessibility/chromevox/options/options.js b/chrome/browser/resources/chromeos/accessibility/chromevox/options/options.js
index 6a79707..a1b4a018 100644
--- a/chrome/browser/resources/chromeos/accessibility/chromevox/options/options.js
+++ b/chrome/browser/resources/chromeos/accessibility/chromevox/options/options.js
@@ -8,6 +8,7 @@
  */
 import {ChromeVoxPrefs} from '../background/prefs.js';
 
+import {AbstractTts} from '../common/abstract_tts.js';
 import {TtsBackground} from '../common/tts_background.js';
 
 /** @const {string} */
diff --git a/chrome/browser/resources/chromeos/accessibility/chromevox/options/options_loader.js b/chrome/browser/resources/chromeos/accessibility/chromevox/options/options_loader.js
index 3bc8cf77..6d265b2 100644
--- a/chrome/browser/resources/chromeos/accessibility/chromevox/options/options_loader.js
+++ b/chrome/browser/resources/chromeos/accessibility/chromevox/options/options_loader.js
@@ -6,7 +6,6 @@
  * @fileoverview Loads the options script.
  */
 
-goog.require('AbstractTts');
 goog.require('BluetoothBrailleDisplayUI');
 goog.require('BrailleTable');
 goog.require('BrailleTranslatorManager');
@@ -16,3 +15,8 @@
 goog.require('ExtensionBridge');
 goog.require('Msgs');
 goog.require('PanelCommand');
+goog.require('PhoneticData');
+goog.require('TtsInterface');
+
+goog.require('constants');
+goog.require('goog.i18n.MessageFormat');
diff --git a/chrome/browser/resources/chromeos/accessibility/chromevox/options/options_test.js b/chrome/browser/resources/chromeos/accessibility/chromevox/options/options_test.js
index ff97fe4..a6b6ec2 100644
--- a/chrome/browser/resources/chromeos/accessibility/chromevox/options/options_test.js
+++ b/chrome/browser/resources/chromeos/accessibility/chromevox/options/options_test.js
@@ -16,6 +16,12 @@
     window.press = this.press;
   }
 
+  /** @override */
+  async setUpDeferred() {
+    await super.setUpDeferred();
+    await importModule('AbstractTts', '/chromevox/common/abstract_tts.js');
+  }
+
   runOnOptionsPage(callback) {
     const mockFeedback = this.createMockFeedback();
     chrome.automation.getDesktop((desktop) => {
diff --git a/chrome/browser/resources/chromeos/accessibility/chromevox/panel/i_search_test.js b/chrome/browser/resources/chromeos/accessibility/chromevox/panel/i_search_test.js
index 4943206..eaea1b8 100644
--- a/chrome/browser/resources/chromeos/accessibility/chromevox/panel/i_search_test.js
+++ b/chrome/browser/resources/chromeos/accessibility/chromevox/panel/i_search_test.js
@@ -75,44 +75,43 @@
 }
 
 
-TEST_F('ChromeVoxISearchTest', 'Simple', function() {
-  this.runWithLoadedTree(this.linksAndHeadingsDoc, function(rootNode) {
-    const handler = new FakeISearchHandler(this);
-    const search = new ISearch(new cursors.Cursor(rootNode, 0));
-    search.handler = handler;
+TEST_F('ChromeVoxISearchTest', 'Simple', async function() {
+  const rootNode = await this.runWithLoadedTree(this.linksAndHeadingsDoc);
+  const handler = new FakeISearchHandler(this);
+  const search = new ISearch(new cursors.Cursor(rootNode, 0));
+  search.handler = handler;
 
-    // Simple forward search.
-    search.search('US', 'forward');
-    handler.expect(
-        'start=6 end=8 text=About US',
-        search.search.bind(search, 'start', 'backward'));
+  // Simple forward search.
+  search.search('US', 'forward');
+  handler.expect(
+      'start=6 end=8 text=About US',
+      search.search.bind(search, 'start', 'backward'));
 
-    handler.expect(
-        'start',
-        // Boundary (beginning).
-        search.search.bind(search, 'foo', 'backward'));
+  handler.expect(
+      'start',
+      // Boundary (beginning).
+      search.search.bind(search, 'foo', 'backward'));
 
-    handler.expect(
-        'boundary=start',
-        // Boundary (end).
-        search.search.bind(search, 'foo', 'forward'));
+  handler.expect(
+      'boundary=start',
+      // Boundary (end).
+      search.search.bind(search, 'foo', 'forward'));
 
-    // Search "focus" doesn't move.
-    handler.expect(
-        'boundary=start',
-        // Mixed case substring.
-        search.search.bind(search, 'bReak', 'forward'));
+  // Search "focus" doesn't move.
+  handler.expect(
+      'boundary=start',
+      // Mixed case substring.
+      search.search.bind(search, 'bReak', 'forward'));
 
-    handler.expect(
-        'start=7 end=12 text=Latest Breaking News',
-        search.search.bind(search, 'bReaki', 'forward'));
+  handler.expect(
+      'start=7 end=12 text=Latest Breaking News',
+      search.search.bind(search, 'bReaki', 'forward'));
 
-    // Incremental search stays on the current node.
-    handler.expect(
-        'start=7 end=13 text=Latest Breaking News',
-        search.search.bind(search, 'bReakio', 'forward'));
+  // Incremental search stays on the current node.
+  handler.expect(
+      'start=7 end=13 text=Latest Breaking News',
+      search.search.bind(search, 'bReakio', 'forward'));
 
-    // No results for the search.
-    handler.expect('boundary=Latest Breaking News');
-  });
+  // No results for the search.
+  handler.expect('boundary=Latest Breaking News');
 });
diff --git a/chrome/browser/resources/chromeos/accessibility/chromevox/panel/panel.js b/chrome/browser/resources/chromeos/accessibility/chromevox/panel/panel.js
index ad8a0d72..4d7fc36 100644
--- a/chrome/browser/resources/chromeos/accessibility/chromevox/panel/panel.js
+++ b/chrome/browser/resources/chromeos/accessibility/chromevox/panel/panel.js
@@ -6,6 +6,8 @@
  * @fileoverview The ChromeVox panel and menus.
  */
 import {GestureCommandData} from '../background/gesture_command_data.js';
+import {KeyMap} from '../background/keymaps/key_map.js';
+import {CommandStore} from '../common/command_store.js';
 
 import {ISearchUI} from './i_search.js';
 import {PanelInterface} from './panel_interface.js';
diff --git a/chrome/browser/resources/chromeos/accessibility/chromevox/panel/panel_loader.js b/chrome/browser/resources/chromeos/accessibility/chromevox/panel/panel_loader.js
index d31eb853..441da62 100644
--- a/chrome/browser/resources/chromeos/accessibility/chromevox/panel/panel_loader.js
+++ b/chrome/browser/resources/chromeos/accessibility/chromevox/panel/panel_loader.js
@@ -11,11 +11,9 @@
 goog.require('AutomationUtil');
 goog.require('BrailleCommandData');
 goog.require('ChromeVoxState');
-goog.require('CommandStore');
 goog.require('EventGenerator');
 goog.require('EventSourceType');
 goog.require('KeyCode');
-goog.require('KeyMap');
 goog.require('KeyUtil');
 goog.require('LocaleOutputHelper');
 goog.require('Msgs');
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 528fd10..f06a41c0 100644
--- a/chrome/browser/resources/chromeos/accessibility/chromevox/panel/panel_test.js
+++ b/chrome/browser/resources/chromeos/accessibility/chromevox/panel/panel_test.js
@@ -88,180 +88,167 @@
   }
 };
 
-TEST_F('ChromeVoxPanelTest', 'ActivateMenu', function() {
-  this.runWithLoadedTree(this.linksDoc, async function(root) {
-    new PanelCommand(PanelCommandType.OPEN_MENUS).send();
-    await this.waitForMenu('panel_search_menu');
-    this.fireMockEvent('ArrowRight')();
-    this.assertActiveMenuItem('panel_menu_jump', 'Go To Beginning Of Table');
-    this.fireMockEvent('ArrowRight')();
-    this.assertActiveMenuItem(
-        'panel_menu_speech', 'Announce Current Battery Status');
-  });
+TEST_F('ChromeVoxPanelTest', 'ActivateMenu', async function() {
+  await this.runWithLoadedTree(this.linksDoc);
+  new PanelCommand(PanelCommandType.OPEN_MENUS).send();
+  await this.waitForMenu('panel_search_menu');
+  this.fireMockEvent('ArrowRight')();
+  this.assertActiveMenuItem('panel_menu_jump', 'Go To Beginning Of Table');
+  this.fireMockEvent('ArrowRight')();
+  this.assertActiveMenuItem(
+      'panel_menu_speech', 'Announce Current Battery Status');
 });
 
 // TODO(https://crbug.com/1299765): Re-enable once flaky timeouts are fixed.
-TEST_F('ChromeVoxPanelTest', 'DISABLED_LinkMenu', function() {
-  this.runWithLoadedTree(this.linksDoc, async function(root) {
-    CommandHandlerInterface.instance.onCommand('showLinksList');
-    await this.waitForMenu('role_link');
-    this.fireMockEvent('ArrowLeft')();
-    this.assertActiveMenuItem('role_landmark', 'No items');
-    this.fireMockEvent('ArrowRight')();
-    this.assertActiveMenuItem('role_link', 'apple Internal link');
-    this.fireMockEvent('ArrowUp')();
-    this.assertActiveMenuItem('role_link', 'banana Internal link');
-  });
+TEST_F('ChromeVoxPanelTest', 'DISABLED_LinkMenu', async function() {
+  await this.runWithLoadedTree(this.linksDoc);
+  CommandHandlerInterface.instance.onCommand('showLinksList');
+  await this.waitForMenu('role_link');
+  this.fireMockEvent('ArrowLeft')();
+  this.assertActiveMenuItem('role_landmark', 'No items');
+  this.fireMockEvent('ArrowRight')();
+  this.assertActiveMenuItem('role_link', 'apple Internal link');
+  this.fireMockEvent('ArrowUp')();
+  this.assertActiveMenuItem('role_link', 'banana Internal link');
 });
 
-TEST_F('ChromeVoxPanelTest', 'FormControlsMenu', function() {
-  this.runWithLoadedTree(
-      `<button>Cancel</button><button>OK</button>`, async function(root) {
-        CommandHandlerInterface.instance.onCommand('showFormsList');
-        await this.waitForMenu('panel_menu_form_controls');
-        this.fireMockEvent('ArrowDown')();
-        this.assertActiveMenuItem('panel_menu_form_controls', 'OK Button');
-        this.fireMockEvent('ArrowUp')();
-        this.assertActiveMenuItem('panel_menu_form_controls', 'Cancel Button');
-      });
+TEST_F('ChromeVoxPanelTest', 'FormControlsMenu', async function() {
+  await this.runWithLoadedTree(`<button>Cancel</button><button>OK</button>`);
+  CommandHandlerInterface.instance.onCommand('showFormsList');
+  await this.waitForMenu('panel_menu_form_controls');
+  this.fireMockEvent('ArrowDown')();
+  this.assertActiveMenuItem('panel_menu_form_controls', 'OK Button');
+  this.fireMockEvent('ArrowUp')();
+  this.assertActiveMenuItem('panel_menu_form_controls', 'Cancel Button');
 });
 
-TEST_F('ChromeVoxPanelTest', 'SearchMenu', function() {
+TEST_F('ChromeVoxPanelTest', 'SearchMenu', async function() {
   const mockFeedback = this.createMockFeedback();
-  this.runWithLoadedTree(this.linksDoc, async function(root) {
-    new PanelCommand(PanelCommandType.OPEN_MENUS).send();
-    await this.waitForMenu('panel_search_menu');
-    await mockFeedback
-        .expectSpeech('Search the menus', /Type to search the menus/)
-        .call(() => {
-          this.fireMockQuery('jump')();
-          this.assertActiveSearchMenuItem('Jump To Details');
-        })
-        .expectSpeech(/Jump/, 'Menu item', /[0-9]+ of [0-9]+/)
-        .call(() => {
-          this.fireMockEvent('ArrowDown')();
-          this.assertActiveSearchMenuItem('Jump To The Bottom Of The Page');
-        })
-        .expectSpeech(/Jump/, 'Menu item', /[0-9]+ of [0-9]+/)
-        .call(() => {
-          this.fireMockEvent('ArrowDown')();
-          this.assertActiveSearchMenuItem('Jump To The Top Of The Page');
-        })
-        .expectSpeech(/Jump/, 'Menu item', /[0-9]+ of [0-9]+/)
-        .call(() => {
-          this.fireMockEvent('ArrowDown')();
-          this.assertActiveSearchMenuItem('Jump To Details');
-        })
-        .expectSpeech(/Jump/, 'Menu item', /[0-9]+ of [0-9]+/)
-        .replay();
-  });
+  await this.runWithLoadedTree(this.linksDoc);
+  new PanelCommand(PanelCommandType.OPEN_MENUS).send();
+  await this.waitForMenu('panel_search_menu');
+  await mockFeedback
+      .expectSpeech('Search the menus', /Type to search the menus/)
+      .call(() => {
+        this.fireMockQuery('jump')();
+        this.assertActiveSearchMenuItem('Jump To Details');
+      })
+      .expectSpeech(/Jump/, 'Menu item', /[0-9]+ of [0-9]+/)
+      .call(() => {
+        this.fireMockEvent('ArrowDown')();
+        this.assertActiveSearchMenuItem('Jump To The Bottom Of The Page');
+      })
+      .expectSpeech(/Jump/, 'Menu item', /[0-9]+ of [0-9]+/)
+      .call(() => {
+        this.fireMockEvent('ArrowDown')();
+        this.assertActiveSearchMenuItem('Jump To The Top Of The Page');
+      })
+      .expectSpeech(/Jump/, 'Menu item', /[0-9]+ of [0-9]+/)
+      .call(() => {
+        this.fireMockEvent('ArrowDown')();
+        this.assertActiveSearchMenuItem('Jump To Details');
+      })
+      .expectSpeech(/Jump/, 'Menu item', /[0-9]+ of [0-9]+/)
+      .replay();
 });
 
 // TODO(crbug.com/1088438): flaky crashes.
-TEST_F('ChromeVoxPanelTest', 'DISABLED_Gestures', function() {
+TEST_F('ChromeVoxPanelTest', 'DISABLED_Gestures', async function() {
   const doGestureAsync = async (gesture) => {
     doGesture(gesture)();
   };
-  this.runWithLoadedTree(
-      `<button>Cancel</button><button>OK</button>`, async function(root) {
-        doGestureAsync(Gesture.TAP4);
-        await this.waitForMenu('panel_search_menu');
-        // GestureCommandHandler behaves in special ways only with range over
-        // the panel. Fake this out by setting range there.
-        const desktop = root.parent.root;
-        const panelNode = desktop.find(
-            {role: 'rootWebArea', attributes: {name: 'ChromeVox Panel'}});
-        ChromeVoxState.instance.setCurrentRange(
-            cursors.Range.fromNode(panelNode));
+  await this.runWithLoadedTree(`<button>Cancel</button><button>OK</button>`);
+  doGestureAsync(Gesture.TAP4);
+  await this.waitForMenu('panel_search_menu');
+  // GestureCommandHandler behaves in special ways only with range over
+  // the panel. Fake this out by setting range there.
+  const desktop = root.parent.root;
+  const panelNode = desktop.find(
+      {role: 'rootWebArea', attributes: {name: 'ChromeVox Panel'}});
+  ChromeVoxState.instance.setCurrentRange(cursors.Range.fromNode(panelNode));
 
-        doGestureAsync(Gesture.SWIPE_RIGHT1);
-        await this.waitForMenu('panel_menu_jump');
+  doGestureAsync(Gesture.SWIPE_RIGHT1);
+  await this.waitForMenu('panel_menu_jump');
 
-        doGestureAsync(Gesture.SWIPE_RIGHT1);
-        await this.waitForMenu('panel_menu_speech');
+  doGestureAsync(Gesture.SWIPE_RIGHT1);
+  await this.waitForMenu('panel_menu_speech');
 
-        doGestureAsync(Gesture.SWIPE_LEFT1);
-        await this.waitForMenu('panel_menu_jump');
-      });
+  doGestureAsync(Gesture.SWIPE_LEFT1);
+  await this.waitForMenu('panel_menu_jump');
 });
 
-TEST_F('ChromeVoxPanelTest', 'InternationalFormControlsMenu', function() {
-  this.runWithLoadedTree(this.internationalButtonDoc, async function(root) {
-    // Turn on language switching and set available voice list.
-    localStorage['languageSwitching'] = 'true';
-    this.getPanelWindow().LocaleOutputHelper.instance.availableVoices_ =
-        [{'lang': 'en-US'}, {'lang': 'es-ES'}];
-    CommandHandlerInterface.instance.onCommand('showFormsList');
-    await this.waitForMenu('panel_menu_form_controls');
-    this.fireMockEvent('ArrowDown')();
-    this.assertActiveMenuItem(
-        'panel_menu_form_controls', 'español: Prueba Button');
-    this.fireMockEvent('ArrowUp')();
-    this.assertActiveMenuItem('panel_menu_form_controls', 'Test Button');
-  });
+TEST_F('ChromeVoxPanelTest', 'InternationalFormControlsMenu', async function() {
+  await this.runWithLoadedTree(this.internationalButtonDoc);
+  // Turn on language switching and set available voice list.
+  localStorage['languageSwitching'] = 'true';
+  this.getPanelWindow().LocaleOutputHelper.instance.availableVoices_ =
+      [{'lang': 'en-US'}, {'lang': 'es-ES'}];
+  CommandHandlerInterface.instance.onCommand('showFormsList');
+  await this.waitForMenu('panel_menu_form_controls');
+  this.fireMockEvent('ArrowDown')();
+  this.assertActiveMenuItem(
+      'panel_menu_form_controls', 'español: Prueba Button');
+  this.fireMockEvent('ArrowUp')();
+  this.assertActiveMenuItem('panel_menu_form_controls', 'Test Button');
 });
 
-TEST_F('ChromeVoxPanelTest', 'ActionsMenu', function() {
-  this.runWithLoadedTree(this.linksDoc, async function(root) {
-    CommandHandlerInterface.instance.onCommand('showActionsMenu');
-    await this.waitForMenu('panel_menu_actions');
-    this.fireMockEvent('ArrowDown')();
-    this.assertActiveMenuItem('panel_menu_actions', 'Start Or End Selection');
-    this.fireMockEvent('ArrowUp')();
-    this.assertActiveMenuItem('panel_menu_actions', 'Click On Current Item');
-  });
+TEST_F('ChromeVoxPanelTest', 'ActionsMenu', async function() {
+  await this.runWithLoadedTree(this.linksDoc);
+  CommandHandlerInterface.instance.onCommand('showActionsMenu');
+  await this.waitForMenu('panel_menu_actions');
+  this.fireMockEvent('ArrowDown')();
+  this.assertActiveMenuItem('panel_menu_actions', 'Start Or End Selection');
+  this.fireMockEvent('ArrowUp')();
+  this.assertActiveMenuItem('panel_menu_actions', 'Click On Current Item');
 });
 
-TEST_F('ChromeVoxPanelTest', 'ShortcutsAreInternationalized', function() {
-  this.runWithLoadedTree(this.linksDoc, async function(root) {
-    new PanelCommand(PanelCommandType.OPEN_MENUS).send();
-    await this.waitForMenu('panel_search_menu');
-    this.fireMockEvent('ArrowRight')();
-    this.assertActiveMenuItem(
-        'panel_menu_jump', 'Go To Beginning Of Table',
-        'Search+Alt+Shift+ArrowLeft');
-    this.fireMockEvent('ArrowRight')();
-    this.assertActiveMenuItem(
-        'panel_menu_speech', 'Announce Current Battery Status',
-        'Search+O, then B');
-    // Skip the tabs menu.
-    this.fireMockEvent('ArrowRight')();
-    this.fireMockEvent('ArrowRight')();
-    this.assertActiveMenuItem(
-        'panel_menu_chromevox', 'Open keyboard shortcuts menu', 'Ctrl+Alt+/');
-  });
+TEST_F('ChromeVoxPanelTest', 'ShortcutsAreInternationalized', async function() {
+  await this.runWithLoadedTree(this.linksDoc);
+  new PanelCommand(PanelCommandType.OPEN_MENUS).send();
+  await this.waitForMenu('panel_search_menu');
+  this.fireMockEvent('ArrowRight')();
+  this.assertActiveMenuItem(
+      'panel_menu_jump', 'Go To Beginning Of Table',
+      'Search+Alt+Shift+ArrowLeft');
+  this.fireMockEvent('ArrowRight')();
+  this.assertActiveMenuItem(
+      'panel_menu_speech', 'Announce Current Battery Status',
+      'Search+O, then B');
+  // Skip the tabs menu.
+  this.fireMockEvent('ArrowRight')();
+  this.fireMockEvent('ArrowRight')();
+  this.assertActiveMenuItem(
+      'panel_menu_chromevox', 'Open keyboard shortcuts menu', 'Ctrl+Alt+/');
 });
 
 // Ensure 'Touch Gestures' is not in the panel menus by default.
 TEST_F(
     'ChromeVoxPanelTest', 'TouchGesturesMenuNotAvailableWhenNotInTouchMode',
-    function() {
-      this.runWithLoadedTree(this.linksDoc, async function(root) {
-        new PanelCommand(PanelCommandType.OPEN_MENUS).send();
-        await this.waitForMenu('panel_search_menu');
-        do {
-          this.fireMockEvent('ArrowRight')();
-          assertFalse(this.isMenuTitleMessage('panel_menu_touchgestures'));
-        } while (!this.isMenuTitleMessage('panel_search_menu'));
-      });
+    async function() {
+      await this.runWithLoadedTree(this.linksDoc);
+      new PanelCommand(PanelCommandType.OPEN_MENUS).send();
+      await this.waitForMenu('panel_search_menu');
+      do {
+        this.fireMockEvent('ArrowRight')();
+        assertFalse(this.isMenuTitleMessage('panel_menu_touchgestures'));
+      } while (!this.isMenuTitleMessage('panel_search_menu'));
     });
 
 // Ensure 'Touch Gesture' is in the panel menus when touch mode is enabled.
 TEST_F(
     'ChromeVoxPanelTest', 'TouchGesturesMenuAvailableWhenInTouchMode',
-    function() {
-      this.runWithLoadedTree(this.linksDoc, async function(root) {
-        this.getPanel().setTouchGestureSourceForTesting();
-        new PanelCommand(PanelCommandType.OPEN_MENUS).send();
-        await this.waitForMenu('panel_search_menu');
+    async function() {
+      await this.runWithLoadedTree(this.linksDoc);
+      this.getPanel().setTouchGestureSourceForTesting();
+      new PanelCommand(PanelCommandType.OPEN_MENUS).send();
+      await this.waitForMenu('panel_search_menu');
 
-        // Look for Touch Gestures menu, fail if getting back to start.
-        do {
-          this.fireMockEvent('ArrowRight')();
-          assertFalse(this.isMenuTitleMessage('panel_search_menu'));
-        } while (!this.isMenuTitleMessage('panel_menu_touchgestures'));
+      // Look for Touch Gestures menu, fail if getting back to start.
+      do {
+        this.fireMockEvent('ArrowRight')();
+        assertFalse(this.isMenuTitleMessage('panel_search_menu'));
+      } while (!this.isMenuTitleMessage('panel_menu_touchgestures'));
 
-        this.assertActiveMenuItem(
-            'panel_menu_touchgestures', 'Click on current item');
-      });
+      this.assertActiveMenuItem(
+          'panel_menu_touchgestures', 'Click on current item');
     });
diff --git a/chrome/browser/resources/chromeos/accessibility/chromevox/panel/tutorial_test.js b/chrome/browser/resources/chromeos/accessibility/chromevox/panel/tutorial_test.js
index c84dcd0..04399a3 100644
--- a/chrome/browser/resources/chromeos/accessibility/chromevox/panel/tutorial_test.js
+++ b/chrome/browser/resources/chromeos/accessibility/chromevox/panel/tutorial_test.js
@@ -77,398 +77,387 @@
   }
 };
 
-TEST_F('ChromeVoxTutorialTest', 'BasicTest', function() {
+TEST_F('ChromeVoxTutorialTest', 'BasicTest', async function() {
   const mockFeedback = this.createMockFeedback();
-  this.runWithLoadedTree(this.simpleDoc, async function(root) {
-    await this.launchAndWaitForTutorial();
-    mockFeedback
-        .expectSpeech(
-            'ChromeVox tutorial', 'Heading 1',
-            'Press Search + Right Arrow, or Search + Left Arrow to browse' +
-                ' topics')
-        .call(doCmd('nextObject'))
-        .expectSpeech('Quick orientation', 'Link')
-        .call(doCmd('nextObject'))
-        .expectSpeech('Essential keys', 'Link')
-        .call(doCmd('nextObject'))
-        .expectSpeech('Navigation', 'Link')
-        .call(doCmd('nextObject'))
-        .expectSpeech('Command references', 'Link')
-        .call(doCmd('nextObject'))
-        .expectSpeech('Sounds and settings', 'Link')
-        .call(doCmd('nextObject'))
-        .expectSpeech('Resources', 'Link')
-        .call(doCmd('nextObject'))
-        .expectSpeech('Exit tutorial', 'Button')
-        .replay();
-  });
+  const root = await this.runWithLoadedTree(this.simpleDoc);
+  await this.launchAndWaitForTutorial();
+  mockFeedback
+      .expectSpeech(
+          'ChromeVox tutorial', 'Heading 1',
+          'Press Search + Right Arrow, or Search + Left Arrow to browse' +
+              ' topics')
+      .call(doCmd('nextObject'))
+      .expectSpeech('Quick orientation', 'Link')
+      .call(doCmd('nextObject'))
+      .expectSpeech('Essential keys', 'Link')
+      .call(doCmd('nextObject'))
+      .expectSpeech('Navigation', 'Link')
+      .call(doCmd('nextObject'))
+      .expectSpeech('Command references', 'Link')
+      .call(doCmd('nextObject'))
+      .expectSpeech('Sounds and settings', 'Link')
+      .call(doCmd('nextObject'))
+      .expectSpeech('Resources', 'Link')
+      .call(doCmd('nextObject'))
+      .expectSpeech('Exit tutorial', 'Button')
+      .replay();
 });
 
 // Tests that different lessons are shown when choosing an experience from the
 // main menu.
 // TODO(crbug.com/1193799): fix ax node errors causing console spew and
 // breaking tests
-TEST_F('ChromeVoxTutorialTest', 'DISABLED_LessonSetTest', function() {
+TEST_F('ChromeVoxTutorialTest', 'DISABLED_LessonSetTest', async function() {
   const mockFeedback = this.createMockFeedback();
-  this.runWithLoadedTree(this.simpleDoc, async function(root) {
-    await this.launchAndWaitForTutorial();
-    const tutorial = this.getTutorial();
-    mockFeedback.expectSpeech('ChromeVox tutorial')
-        .call(doCmd('nextObject'))
-        .expectSpeech('Quick orientation')
-        .call(doCmd('forceClickOnCurrentItem'))
-        .expectSpeech(/Quick Orientation Tutorial, [0-9]+ Lessons/)
-        .expectSpeech(
-            'Press Search + Right Arrow, or Search + Left Arrow to browse ' +
-            'lessons for this topic')
-        .call(doCmd('nextObject'))
-        .expectSpeech('Welcome to ChromeVox!')
-        .call(() => {
-          // Call from the tutorial directly, instead of navigating to and
-          // clicking on the main menu button.
-          tutorial.showMainMenu_();
-        })
-        .expectSpeech('ChromeVox tutorial')
-        .call(doCmd('nextObject'))
-        .expectSpeech('Quick orientation')
-        .call(doCmd('nextObject'))
-        .expectSpeech('Essential keys', 'Link')
-        .call(doCmd('forceClickOnCurrentItem'))
-        .expectSpeech(/Essential Keys Tutorial, [0-9]+ Lessons/)
-        .call(doCmd('nextObject'))
-        .expectSpeech('On, Off, and Stop')
-        .replay();
-  });
+  const root = await this.runWithLoadedTree(this.simpleDoc);
+  await this.launchAndWaitForTutorial();
+  const tutorial = this.getTutorial();
+  mockFeedback.expectSpeech('ChromeVox tutorial')
+      .call(doCmd('nextObject'))
+      .expectSpeech('Quick orientation')
+      .call(doCmd('forceClickOnCurrentItem'))
+      .expectSpeech(/Quick Orientation Tutorial, [0-9]+ Lessons/)
+      .expectSpeech(
+          'Press Search + Right Arrow, or Search + Left Arrow to browse ' +
+          'lessons for this topic')
+      .call(doCmd('nextObject'))
+      .expectSpeech('Welcome to ChromeVox!')
+      .call(() => {
+        // Call from the tutorial directly, instead of navigating to and
+        // clicking on the main menu button.
+        tutorial.showMainMenu_();
+      })
+      .expectSpeech('ChromeVox tutorial')
+      .call(doCmd('nextObject'))
+      .expectSpeech('Quick orientation')
+      .call(doCmd('nextObject'))
+      .expectSpeech('Essential keys', 'Link')
+      .call(doCmd('forceClickOnCurrentItem'))
+      .expectSpeech(/Essential Keys Tutorial, [0-9]+ Lessons/)
+      .call(doCmd('nextObject'))
+      .expectSpeech('On, Off, and Stop')
+      .replay();
 });
 
 // Tests that a static lesson does not show the 'Practice area' button.
 // TODO(crbug.com/1193799): fix ax node errors causing console spew and
 // breaking tests
-TEST_F('ChromeVoxTutorialTest', 'DISABLED_NoPracticeAreaTest', function() {
+TEST_F('ChromeVoxTutorialTest', 'DISABLED_NoPracticeAreaTest', async function() {
   const mockFeedback = this.createMockFeedback();
-  this.runWithLoadedTree(this.simpleDoc, async function(root) {
-    await this.launchAndWaitForTutorial();
-    const tutorial = this.getTutorial();
-    mockFeedback.expectSpeech('ChromeVox tutorial')
-        .call(doCmd('nextObject'))
-        .expectSpeech('Quick orientation')
-        .call(doCmd('nextObject'))
-        .expectSpeech('Essential keys')
-        .call(doCmd('forceClickOnCurrentItem'))
-        .expectSpeech(/Essential Keys Tutorial, [0-9]+ Lessons/)
-        .call(() => {
-          tutorial.showLesson_(0);
-        })
-        .expectSpeech(
-            'On, Off, and Stop', 'Heading 1',
-            ' Press Search + Right Arrow, or Search + Left Arrow to navigate ' +
-                'this lesson ')
-        .call(doCmd('nextButton'))
-        .expectSpeech('Next lesson')
-        .replay();
-  });
+  const root = await this.runWithLoadedTree(this.simpleDoc);
+  await this.launchAndWaitForTutorial();
+  const tutorial = this.getTutorial();
+  mockFeedback.expectSpeech('ChromeVox tutorial')
+      .call(doCmd('nextObject'))
+      .expectSpeech('Quick orientation')
+      .call(doCmd('nextObject'))
+      .expectSpeech('Essential keys')
+      .call(doCmd('forceClickOnCurrentItem'))
+      .expectSpeech(/Essential Keys Tutorial, [0-9]+ Lessons/)
+      .call(() => {
+        tutorial.showLesson_(0);
+      })
+      .expectSpeech(
+          'On, Off, and Stop', 'Heading 1',
+          ' Press Search + Right Arrow, or Search + Left Arrow to navigate ' +
+              'this lesson ')
+      .call(doCmd('nextButton'))
+      .expectSpeech('Next lesson')
+      .replay();
 });
 
 // Tests that an interactive lesson shows the 'Practice area' button.
 // TODO(crbug.com/1193799): fix ax node errors causing console spew and
 // breaking tests
-TEST_F('ChromeVoxTutorialTest', 'DISABLED_HasPracticeAreaTest', function() {
-  const mockFeedback = this.createMockFeedback();
-  this.runWithLoadedTree(this.simpleDoc, async function(root) {
-    await this.launchAndWaitForTutorial();
-    const tutorial = this.getTutorial();
-    mockFeedback.expectSpeech('ChromeVox tutorial')
-        .call(doCmd('nextObject'))
-        .expectSpeech('Quick orientation')
-        .call(doCmd('nextObject'))
-        .expectSpeech('Essential keys')
-        .call(doCmd('nextObject'))
-        .expectSpeech('Navigation')
-        .call(doCmd('forceClickOnCurrentItem'))
-        .expectSpeech(/Navigation Tutorial, [0-9]+ Lessons/)
-        .call(() => {
-          tutorial.showLesson_(1);
-        })
-        .expectSpeech('Jump Commands', 'Heading 1')
-        .call(doCmd('nextButton'))
-        .expectSpeech('Practice area')
-        .replay();
-  });
-});
+TEST_F(
+    'ChromeVoxTutorialTest', 'DISABLED_HasPracticeAreaTest', async function() {
+      const mockFeedback = this.createMockFeedback();
+      const root = await this.runWithLoadedTree(this.simpleDoc);
+      await this.launchAndWaitForTutorial();
+      const tutorial = this.getTutorial();
+      mockFeedback.expectSpeech('ChromeVox tutorial')
+          .call(doCmd('nextObject'))
+          .expectSpeech('Quick orientation')
+          .call(doCmd('nextObject'))
+          .expectSpeech('Essential keys')
+          .call(doCmd('nextObject'))
+          .expectSpeech('Navigation')
+          .call(doCmd('forceClickOnCurrentItem'))
+          .expectSpeech(/Navigation Tutorial, [0-9]+ Lessons/)
+          .call(() => {
+            tutorial.showLesson_(1);
+          })
+          .expectSpeech('Jump Commands', 'Heading 1')
+          .call(doCmd('nextButton'))
+          .expectSpeech('Practice area')
+          .replay();
+    });
 
 // Tests nudges given in the general tutorial context.
 // The first three nudges should read the current item with full context.
 // Afterward, general hints will be given about using ChromeVox. Lastly,
 // we will give a hint for exiting the tutorial.
-TEST_F('ChromeVoxTutorialTest', 'GeneralNudgesTest', function() {
+TEST_F('ChromeVoxTutorialTest', 'GeneralNudgesTest', async function() {
   const mockFeedback = this.createMockFeedback();
-  this.runWithLoadedTree(this.simpleDoc, async function(root) {
-    await this.launchAndWaitForTutorial();
-    const tutorial = this.getTutorial();
-    const giveNudge = () => {
-      tutorial.giveNudge();
-    };
-    mockFeedback.expectSpeech('ChromeVox tutorial');
-    for (let i = 0; i < 3; ++i) {
-      mockFeedback.call(giveNudge).expectSpeech(
-          'ChromeVox tutorial', 'Heading 1');
-    }
-    mockFeedback.call(giveNudge)
-        .expectSpeech('Hint: Hold Search and press the arrow keys to navigate.')
-        .call(giveNudge)
-        .expectSpeech(
-            'Hint: Press Search + Space to activate the current item.')
-        .call(giveNudge)
-        .expectSpeech(
-            'Hint: Press Escape if you would like to exit this tutorial.')
-        .replay();
-  });
+  const root = await this.runWithLoadedTree(this.simpleDoc);
+  await this.launchAndWaitForTutorial();
+  const tutorial = this.getTutorial();
+  const giveNudge = () => {
+    tutorial.giveNudge();
+  };
+  mockFeedback.expectSpeech('ChromeVox tutorial');
+  for (let i = 0; i < 3; ++i) {
+    mockFeedback.call(giveNudge).expectSpeech(
+        'ChromeVox tutorial', 'Heading 1');
+  }
+  mockFeedback.call(giveNudge)
+      .expectSpeech('Hint: Hold Search and press the arrow keys to navigate.')
+      .call(giveNudge)
+      .expectSpeech('Hint: Press Search + Space to activate the current item.')
+      .call(giveNudge)
+      .expectSpeech(
+          'Hint: Press Escape if you would like to exit this tutorial.')
+      .replay();
 });
 
 // Tests nudges given in the practice area context. Note, each practice area
 // can have different nudge messages; this test confirms that nudges given in
 // the practice area differ from those given in the general tutorial context.
-TEST_F('ChromeVoxTutorialTest', 'DISABLED_PracticeAreaNudgesTest', function() {
-  const mockFeedback = this.createMockFeedback();
-  this.runWithLoadedTree(this.simpleDoc, async function(root) {
-    await this.launchAndWaitForTutorial();
-    const tutorial = this.getTutorial();
-    const giveNudge = () => {
-      tutorial.giveNudge();
-    };
-    mockFeedback.expectSpeech('ChromeVox tutorial')
-        .call(doCmd('nextObject'))
-        .expectSpeech('Quick orientation')
-        .call(doCmd('nextObject'))
-        .expectSpeech('Essential keys')
-        .call(doCmd('nextObject'))
-        .expectSpeech('Navigation')
-        .call(doCmd('forceClickOnCurrentItem'))
-        .expectSpeech(/Navigation Tutorial, [0-9]+ Lessons/)
-        .call(() => {
-          tutorial.showLesson_(0);
-        })
-        .expectSpeech('Basic Navigation', 'Heading 1')
-        .call(doCmd('nextButton'))
-        .expectSpeech('Practice area')
-        .call(doCmd('forceClickOnCurrentItem'))
-        .expectSpeech(/Try using basic navigation to navigate/)
-        .call(giveNudge)
-        .expectSpeech(
-            'Try pressing Search + left/right arrow. The search key is ' +
-            'directly above the shift key')
-        .call(giveNudge)
-        .expectSpeech('Press Search + Space to activate the current item.')
-        .replay();
-  });
-});
+TEST_F(
+    'ChromeVoxTutorialTest', 'DISABLED_PracticeAreaNudgesTest',
+    async function() {
+      const mockFeedback = this.createMockFeedback();
+      const root = await this.runWithLoadedTree(this.simpleDoc);
+      await this.launchAndWaitForTutorial();
+      const tutorial = this.getTutorial();
+      const giveNudge = () => {
+        tutorial.giveNudge();
+      };
+      mockFeedback.expectSpeech('ChromeVox tutorial')
+          .call(doCmd('nextObject'))
+          .expectSpeech('Quick orientation')
+          .call(doCmd('nextObject'))
+          .expectSpeech('Essential keys')
+          .call(doCmd('nextObject'))
+          .expectSpeech('Navigation')
+          .call(doCmd('forceClickOnCurrentItem'))
+          .expectSpeech(/Navigation Tutorial, [0-9]+ Lessons/)
+          .call(() => {
+            tutorial.showLesson_(0);
+          })
+          .expectSpeech('Basic Navigation', 'Heading 1')
+          .call(doCmd('nextButton'))
+          .expectSpeech('Practice area')
+          .call(doCmd('forceClickOnCurrentItem'))
+          .expectSpeech(/Try using basic navigation to navigate/)
+          .call(giveNudge)
+          .expectSpeech(
+              'Try pressing Search + left/right arrow. The search key is ' +
+              'directly above the shift key')
+          .call(giveNudge)
+          .expectSpeech('Press Search + Space to activate the current item.')
+          .replay();
+    });
 
 // Tests that the tutorial closes when the 'Exit tutorial' button is clicked.
-TEST_F('ChromeVoxTutorialTest', 'ExitButtonTest', function() {
+TEST_F('ChromeVoxTutorialTest', 'ExitButtonTest', async function() {
   const mockFeedback = this.createMockFeedback();
-  this.runWithLoadedTree(this.simpleDoc, async function(root) {
-    await this.launchAndWaitForTutorial();
-    const tutorial = this.getTutorial();
-    mockFeedback.expectSpeech('ChromeVox tutorial')
-        .call(doCmd('previousButton'))
-        .expectSpeech('Exit tutorial')
-        .call(doCmd('forceClickOnCurrentItem'))
-        .expectSpeech('Some web content')
-        .replay();
-  });
+  const root = await this.runWithLoadedTree(this.simpleDoc);
+  await this.launchAndWaitForTutorial();
+  const tutorial = this.getTutorial();
+  mockFeedback.expectSpeech('ChromeVox tutorial')
+      .call(doCmd('previousButton'))
+      .expectSpeech('Exit tutorial')
+      .call(doCmd('forceClickOnCurrentItem'))
+      .expectSpeech('Some web content')
+      .replay();
 });
 
 // Tests that the tutorial closes when Escape is pressed.
-TEST_F('ChromeVoxTutorialTest', 'EscapeTest', function() {
+TEST_F('ChromeVoxTutorialTest', 'EscapeTest', async function() {
   const mockFeedback = this.createMockFeedback();
-  this.runWithLoadedTree(this.simpleDoc, async function(root) {
-    await this.launchAndWaitForTutorial();
-    const tutorial = this.getTutorial();
-    mockFeedback.expectSpeech('ChromeVox tutorial')
-        .call(() => {
-          // Press Escape.
-          tutorial.onKeyDown({
-            key: 'Escape',
-            preventDefault: () => {},
-            stopPropagation: () => {}
-          });
-        })
-        .expectSpeech('Some web content')
-        .replay();
-  });
+  const root = await this.runWithLoadedTree(this.simpleDoc);
+  await this.launchAndWaitForTutorial();
+  const tutorial = this.getTutorial();
+  mockFeedback.expectSpeech('ChromeVox tutorial')
+      .call(() => {
+        // Press Escape.
+        tutorial.onKeyDown({
+          key: 'Escape',
+          preventDefault: () => {},
+          stopPropagation: () => {}
+        });
+      })
+      .expectSpeech('Some web content')
+      .replay();
 });
 
 // Tests that the main menu button navigates the user to the main menu screen.
 // TODO(crbug.com/1193799): fix ax node errors causing console spew and
 // breaking tests
-TEST_F('ChromeVoxTutorialTest', 'DISABLED_MainMenuButton', function() {
+TEST_F('ChromeVoxTutorialTest', 'DISABLED_MainMenuButton', async function() {
   const mockFeedback = this.createMockFeedback();
-  this.runWithLoadedTree(this.simpleDoc, async function(root) {
-    await this.launchAndWaitForTutorial();
-    const tutorial = this.getTutorial();
-    mockFeedback.expectSpeech('ChromeVox tutorial')
-        .call(this.assertActiveScreen.bind(this, 'main_menu'))
-        .call(doCmd('nextObject'))
-        .expectSpeech('Quick orientation')
-        .call(doCmd('nextObject'))
-        .expectSpeech('Essential keys')
-        .call(doCmd('forceClickOnCurrentItem'))
-        .expectSpeech(/Essential Keys Tutorial, [0-9]+ Lessons/)
-        .call(this.assertActiveScreen.bind(this, 'lesson_menu'))
-        .call(doCmd('previousButton'))
-        .expectSpeech('Exit tutorial')
-        .call(doCmd('previousButton'))
-        .expectSpeech('Main menu')
-        .call(doCmd('forceClickOnCurrentItem'))
-        .expectSpeech('ChromeVox tutorial')
-        .call(this.assertActiveScreen.bind(this, 'main_menu'))
-        .replay();
-  });
+  const root = await this.runWithLoadedTree(this.simpleDoc);
+  await this.launchAndWaitForTutorial();
+  const tutorial = this.getTutorial();
+  mockFeedback.expectSpeech('ChromeVox tutorial')
+      .call(this.assertActiveScreen.bind(this, 'main_menu'))
+      .call(doCmd('nextObject'))
+      .expectSpeech('Quick orientation')
+      .call(doCmd('nextObject'))
+      .expectSpeech('Essential keys')
+      .call(doCmd('forceClickOnCurrentItem'))
+      .expectSpeech(/Essential Keys Tutorial, [0-9]+ Lessons/)
+      .call(this.assertActiveScreen.bind(this, 'lesson_menu'))
+      .call(doCmd('previousButton'))
+      .expectSpeech('Exit tutorial')
+      .call(doCmd('previousButton'))
+      .expectSpeech('Main menu')
+      .call(doCmd('forceClickOnCurrentItem'))
+      .expectSpeech('ChromeVox tutorial')
+      .call(this.assertActiveScreen.bind(this, 'main_menu'))
+      .replay();
 });
 
 // Tests that the all lessons button navigates the user to the lesson menu
 // screen.
 // TODO(crbug.com/1193799): fix ax node errors causing console spew and
 // breaking tests
-TEST_F('ChromeVoxTutorialTest', 'DISABLED_AllLessonsButton', function() {
+TEST_F('ChromeVoxTutorialTest', 'DISABLED_AllLessonsButton', async function() {
   const mockFeedback = this.createMockFeedback();
-  this.runWithLoadedTree(this.simpleDoc, async function(root) {
-    await this.launchAndWaitForTutorial();
-    const tutorial = this.getTutorial();
-    mockFeedback.expectSpeech('ChromeVox tutorial')
-        .call(this.assertActiveScreen.bind(this, 'main_menu'))
-        .call(doCmd('nextObject'))
-        .expectSpeech('Quick orientation')
-        .call(doCmd('nextObject'))
-        .expectSpeech('Essential keys')
-        .call(doCmd('forceClickOnCurrentItem'))
-        .expectSpeech(/Essential Keys Tutorial, [0-9]+ Lessons/)
-        .call(this.assertActiveScreen.bind(this, 'lesson_menu'))
-        .call(doCmd('nextObject'))
-        .expectSpeech('On, Off, and Stop')
-        .call(doCmd('forceClickOnCurrentItem'))
-        .expectSpeech('On, Off, and Stop', 'Heading 1')
-        .call(this.assertActiveScreen.bind(this, 'lesson'))
-        .call(doCmd('nextButton'))
-        .expectSpeech('Next lesson')
-        .call(doCmd('nextButton'))
-        .expectSpeech('All lessons')
-        .call(doCmd('forceClickOnCurrentItem'))
-        .expectSpeech(/Essential Keys Tutorial, [0-9]+ Lessons/)
-        .call(this.assertActiveScreen.bind(this, 'lesson_menu'))
-        .replay();
-  });
+  const root = await this.runWithLoadedTree(this.simpleDoc);
+  await this.launchAndWaitForTutorial();
+  const tutorial = this.getTutorial();
+  mockFeedback.expectSpeech('ChromeVox tutorial')
+      .call(this.assertActiveScreen.bind(this, 'main_menu'))
+      .call(doCmd('nextObject'))
+      .expectSpeech('Quick orientation')
+      .call(doCmd('nextObject'))
+      .expectSpeech('Essential keys')
+      .call(doCmd('forceClickOnCurrentItem'))
+      .expectSpeech(/Essential Keys Tutorial, [0-9]+ Lessons/)
+      .call(this.assertActiveScreen.bind(this, 'lesson_menu'))
+      .call(doCmd('nextObject'))
+      .expectSpeech('On, Off, and Stop')
+      .call(doCmd('forceClickOnCurrentItem'))
+      .expectSpeech('On, Off, and Stop', 'Heading 1')
+      .call(this.assertActiveScreen.bind(this, 'lesson'))
+      .call(doCmd('nextButton'))
+      .expectSpeech('Next lesson')
+      .call(doCmd('nextButton'))
+      .expectSpeech('All lessons')
+      .call(doCmd('forceClickOnCurrentItem'))
+      .expectSpeech(/Essential Keys Tutorial, [0-9]+ Lessons/)
+      .call(this.assertActiveScreen.bind(this, 'lesson_menu'))
+      .replay();
 });
 
 // Tests that the next and previous lesson buttons navigate properly.
 // TODO(crbug.com/1193799): fix ax node errors causing console spew and
 // breaking tests
-TEST_F('ChromeVoxTutorialTest', 'DISABLED_NextPreviousButtons', function() {
-  const mockFeedback = this.createMockFeedback();
-  this.runWithLoadedTree(this.simpleDoc, async function(root) {
-    await this.launchAndWaitForTutorial();
-    const tutorial = this.getTutorial();
-    mockFeedback.expectSpeech('ChromeVox tutorial')
-        .call(() => {
-          tutorial.curriculum = 'essential_keys';
-          tutorial.showLesson_(0);
-          this.assertActiveLessonIndex(0);
-          this.assertActiveScreen('lesson');
-        })
-        .expectSpeech('On, Off, and Stop', 'Heading 1')
-        .call(doCmd('nextButton'))
-        .expectSpeech('Next lesson')
-        .call(doCmd('forceClickOnCurrentItem'))
-        .expectSpeech('The ChromeVox modifier key', 'Heading 1')
-        .call(this.assertActiveLessonIndex.bind(this, 1))
-        .call(doCmd('nextButton'))
-        .expectSpeech('Previous lesson')
-        .call(doCmd('forceClickOnCurrentItem'))
-        .expectSpeech('On, Off, and Stop', 'Heading 1')
-        .call(this.assertActiveLessonIndex.bind(this, 0))
-        .replay();
-  });
-});
+TEST_F(
+    'ChromeVoxTutorialTest', 'DISABLED_NextPreviousButtons', async function() {
+      const mockFeedback = this.createMockFeedback();
+      const root = await this.runWithLoadedTree(this.simpleDoc);
+      await this.launchAndWaitForTutorial();
+      const tutorial = this.getTutorial();
+      mockFeedback.expectSpeech('ChromeVox tutorial')
+          .call(() => {
+            tutorial.curriculum = 'essential_keys';
+            tutorial.showLesson_(0);
+            this.assertActiveLessonIndex(0);
+            this.assertActiveScreen('lesson');
+          })
+          .expectSpeech('On, Off, and Stop', 'Heading 1')
+          .call(doCmd('nextButton'))
+          .expectSpeech('Next lesson')
+          .call(doCmd('forceClickOnCurrentItem'))
+          .expectSpeech('The ChromeVox modifier key', 'Heading 1')
+          .call(this.assertActiveLessonIndex.bind(this, 1))
+          .call(doCmd('nextButton'))
+          .expectSpeech('Previous lesson')
+          .call(doCmd('forceClickOnCurrentItem'))
+          .expectSpeech('On, Off, and Stop', 'Heading 1')
+          .call(this.assertActiveLessonIndex.bind(this, 0))
+          .replay();
+    });
 
 // Tests that the title of an interactive lesson is read when shown.
-TEST_F('ChromeVoxTutorialTest', 'AutoReadTitle', function() {
+TEST_F('ChromeVoxTutorialTest', 'AutoReadTitle', async function() {
   const mockFeedback = this.createMockFeedback();
-  this.runWithLoadedTree(this.simpleDoc, async function(root) {
-    await this.launchAndWaitForTutorial();
-    const tutorial = this.getTutorial();
-    mockFeedback.expectSpeech('ChromeVox tutorial')
-        .call(doCmd('nextObject'))
-        .expectSpeech('Quick orientation')
-        .call(doCmd('forceClickOnCurrentItem'))
-        .expectSpeech(/Quick Orientation Tutorial, [0-9]+ Lessons/)
-        .call(doCmd('nextObject'))
-        .expectSpeech('Welcome to ChromeVox!')
-        .call(doCmd('forceClickOnCurrentItem'))
-        .expectSpeech('Welcome to ChromeVox!')
-        .expectSpeech(
-            'Welcome to the ChromeVox tutorial. To exit this tutorial at any ' +
-            'time, press the Escape key on the top left corner of the ' +
-            'keyboard. To turn off ChromeVox, hold Control and Alt, and ' +
-            `press Z. When you're ready, use the spacebar to move to the ` +
-            'next lesson.')
-        .replay();
-  });
+  const root = await this.runWithLoadedTree(this.simpleDoc);
+  await this.launchAndWaitForTutorial();
+  const tutorial = this.getTutorial();
+  mockFeedback.expectSpeech('ChromeVox tutorial')
+      .call(doCmd('nextObject'))
+      .expectSpeech('Quick orientation')
+      .call(doCmd('forceClickOnCurrentItem'))
+      .expectSpeech(/Quick Orientation Tutorial, [0-9]+ Lessons/)
+      .call(doCmd('nextObject'))
+      .expectSpeech('Welcome to ChromeVox!')
+      .call(doCmd('forceClickOnCurrentItem'))
+      .expectSpeech('Welcome to ChromeVox!')
+      .expectSpeech(
+          'Welcome to the ChromeVox tutorial. To exit this tutorial at any ' +
+          'time, press the Escape key on the top left corner of the ' +
+          'keyboard. To turn off ChromeVox, hold Control and Alt, and ' +
+          `press Z. When you're ready, use the spacebar to move to the ` +
+          'next lesson.')
+      .replay();
 });
 
 // Tests that we read a hint for navigating a lesson when it is shown.
 // TODO(crbug.com/1193799): fix ax node errors causing console spew and
 // breaking tests
-TEST_F('ChromeVoxTutorialTest', 'DISABLED_LessonHint', function() {
+TEST_F('ChromeVoxTutorialTest', 'DISABLED_LessonHint', async function() {
   const mockFeedback = this.createMockFeedback();
-  this.runWithLoadedTree(this.simpleDoc, async function(root) {
-    await this.launchAndWaitForTutorial();
-    const tutorial = this.getTutorial();
-    mockFeedback.expectSpeech('ChromeVox tutorial')
-        .call(doCmd('nextObject'))
-        .expectSpeech('Quick orientation')
-        .call(doCmd('nextObject'))
-        .expectSpeech('Essential keys')
-        .call(doCmd('forceClickOnCurrentItem'))
-        .expectSpeech(/Essential Keys Tutorial, [0-9]+ Lessons/)
-        .call(() => {
-          tutorial.showLesson_(0);
-        })
-        .expectSpeech('On, Off, and Stop', 'Heading 1')
-        .expectSpeech(
-            ' Press Search + Right Arrow, or Search + Left Arrow to navigate' +
-            ' this lesson ')
-        .replay();
-  });
+  const root = await this.runWithLoadedTree(this.simpleDoc);
+  await this.launchAndWaitForTutorial();
+  const tutorial = this.getTutorial();
+  mockFeedback.expectSpeech('ChromeVox tutorial')
+      .call(doCmd('nextObject'))
+      .expectSpeech('Quick orientation')
+      .call(doCmd('nextObject'))
+      .expectSpeech('Essential keys')
+      .call(doCmd('forceClickOnCurrentItem'))
+      .expectSpeech(/Essential Keys Tutorial, [0-9]+ Lessons/)
+      .call(() => {
+        tutorial.showLesson_(0);
+      })
+      .expectSpeech('On, Off, and Stop', 'Heading 1')
+      .expectSpeech(
+          ' Press Search + Right Arrow, or Search + Left Arrow to navigate' +
+          ' this lesson ')
+      .replay();
 });
 
 // Tests for correct speech and earcons on the earcons lesson.
-TEST_F('ChromeVoxTutorialTest', 'EarconLesson', function() {
+TEST_F('ChromeVoxTutorialTest', 'EarconLesson', async function() {
   const mockFeedback = this.createMockFeedback();
-  this.runWithLoadedTree(this.simpleDoc, async function(root) {
-    await this.launchAndWaitForTutorial();
-    const tutorial = this.getTutorial();
-    const nextObjectAndExpectSpeechAndEarcon = (speech, earcon) => {
-      mockFeedback.call(doCmd('nextObject'))
-          .expectSpeech(speech)
-          .expectEarcon(earcon);
-    };
-    mockFeedback.expectSpeech('ChromeVox tutorial')
-        .call(() => {
-          // Show the lesson.
-          tutorial.curriculum = 'sounds_and_settings';
-          tutorial.showLesson_(0);
-        })
-        .expectSpeech('Sounds')
-        .call(doCmd('nextObject'))
-        .expectSpeech(new RegExp(
-            'ChromeVox uses sounds to give you essential and additional ' +
-            'information.'));
-    nextObjectAndExpectSpeechAndEarcon('A modal alert', Earcon.ALERT_MODAL);
-    nextObjectAndExpectSpeechAndEarcon(
-        'A non modal alert', Earcon.ALERT_NONMODAL);
-    nextObjectAndExpectSpeechAndEarcon('A button', Earcon.BUTTON);
-    mockFeedback.replay();
-  });
+  const root = await this.runWithLoadedTree(this.simpleDoc);
+  await this.launchAndWaitForTutorial();
+  const tutorial = this.getTutorial();
+  const nextObjectAndExpectSpeechAndEarcon = (speech, earcon) => {
+    mockFeedback.call(doCmd('nextObject'))
+        .expectSpeech(speech)
+        .expectEarcon(earcon);
+  };
+  mockFeedback.expectSpeech('ChromeVox tutorial')
+      .call(() => {
+        // Show the lesson.
+        tutorial.curriculum = 'sounds_and_settings';
+        tutorial.showLesson_(0);
+      })
+      .expectSpeech('Sounds')
+      .call(doCmd('nextObject'))
+      .expectSpeech(new RegExp(
+          'ChromeVox uses sounds to give you essential and additional ' +
+          'information.'));
+  nextObjectAndExpectSpeechAndEarcon('A modal alert', Earcon.ALERT_MODAL);
+  nextObjectAndExpectSpeechAndEarcon(
+      'A non modal alert', Earcon.ALERT_NONMODAL);
+  nextObjectAndExpectSpeechAndEarcon('A button', Earcon.BUTTON);
+  mockFeedback.replay();
 });
 
 // Tests that a lesson from the quick orientation blocks ChromeVox execution
@@ -476,308 +465,298 @@
 // TODO(crbug.com/1193799): fix ax node errors causing console spew and
 // breaking tests
 TEST_F(
-    'ChromeVoxTutorialTest', 'DISABLED_QuickOrientationLessonTest', function() {
+    'ChromeVoxTutorialTest', 'DISABLED_QuickOrientationLessonTest',
+    async function() {
       const mockFeedback = this.createMockFeedback();
-      this.runWithLoadedTree(this.simpleDoc, async function(root) {
-        await this.launchAndWaitForTutorial();
-        const tutorial = this.getTutorial();
-        const keyboardHandler = ChromeVoxState.instance.keyboardHandler_;
+      const root = await this.runWithLoadedTree(this.simpleDoc);
+      await this.launchAndWaitForTutorial();
+      const tutorial = this.getTutorial();
+      const keyboardHandler = ChromeVoxState.instance.keyboardHandler_;
 
-        // Helper functions. For this test, activate commands by hooking into
-        // the BackgroundKeyboardHandler. This is necessary because
-        // UserActionMonitor intercepts key sequences before they are routed to
-        // CommandHandler.
-        const getRangeStartNode = () => {
-          return ChromeVoxState.instance.getCurrentRange().start.node;
-        };
+      // Helper functions. For this test, activate commands by hooking into
+      // the BackgroundKeyboardHandler. This is necessary because
+      // UserActionMonitor intercepts key sequences before they are routed to
+      // CommandHandler.
+      const getRangeStartNode = () => {
+        return ChromeVoxState.instance.getCurrentRange().start.node;
+      };
 
-        const simulateKeyPress = (keyCode, opt_modifiers) => {
-          const keyEvent = TestUtils.createMockKeyEvent(keyCode, opt_modifiers);
-          keyboardHandler.onKeyDown(keyEvent);
-          keyboardHandler.onKeyUp(keyEvent);
-        };
+      const simulateKeyPress = (keyCode, opt_modifiers) => {
+        const keyEvent = TestUtils.createMockKeyEvent(keyCode, opt_modifiers);
+        keyboardHandler.onKeyDown(keyEvent);
+        keyboardHandler.onKeyUp(keyEvent);
+      };
 
-        let firstLessonNode;
-        await mockFeedback.expectSpeech('ChromeVox tutorial')
-            .call(doCmd('nextObject'))
-            .expectSpeech('Quick orientation')
-            .call(doCmd('forceClickOnCurrentItem'))
-            .expectSpeech(/Quick Orientation Tutorial, [0-9]+ Lessons/)
-            .call(doCmd('nextObject'))
-            .expectSpeech('Welcome to ChromeVox!')
-            .call(doCmd('forceClickOnCurrentItem'))
-            .expectSpeech(/Welcome to the ChromeVox tutorial./)
-            .call(() => {
-              assertEquals(0, tutorial.activeLessonId);
-              firstLessonNode = getRangeStartNode();
-            })
-            .call(simulateKeyPress.bind(
-                this, KeyCode.RIGHT, {searchKeyHeld: true}))
-            .call(() => {
-              assertEquals(firstLessonNode, getRangeStartNode());
-              assertEquals(0, tutorial.activeLessonId);
-            })
-            .call(simulateKeyPress.bind(
-                this, KeyCode.LEFT, {searchKeyHeld: true}))
-            .call(() => {
-              assertEquals(firstLessonNode, getRangeStartNode());
-              assertEquals(0, tutorial.activeLessonId);
-            })
-            // Pressing space, which is the desired key sequence, should move us
-            // to the next lesson.
-            .call(simulateKeyPress.bind(this, KeyCode.SPACE, {}))
-            .expectSpeech('Essential Keys: Control')
-            .expectSpeech(/Let's start with a few keys you'll use regularly./)
-            .call(() => {
-              assertEquals(1, tutorial.activeLessonId);
-              assertNotEquals(firstLessonNode, getRangeStartNode());
-            })
-            // Pressing control, which is the desired key sequence, should move
-            // us to the next lesson.
-            .call(simulateKeyPress.bind(this, KeyCode.CONTROL, {}))
-            .expectSpeech('Essential Keys: Shift')
-            .call(() => {
-              assertEquals(2, tutorial.activeLessonId);
-            })
-            .replay();
-      });
+      let firstLessonNode;
+      await mockFeedback.expectSpeech('ChromeVox tutorial')
+          .call(doCmd('nextObject'))
+          .expectSpeech('Quick orientation')
+          .call(doCmd('forceClickOnCurrentItem'))
+          .expectSpeech(/Quick Orientation Tutorial, [0-9]+ Lessons/)
+          .call(doCmd('nextObject'))
+          .expectSpeech('Welcome to ChromeVox!')
+          .call(doCmd('forceClickOnCurrentItem'))
+          .expectSpeech(/Welcome to the ChromeVox tutorial./)
+          .call(() => {
+            assertEquals(0, tutorial.activeLessonId);
+            firstLessonNode = getRangeStartNode();
+          })
+          .call(
+              simulateKeyPress.bind(this, KeyCode.RIGHT, {searchKeyHeld: true}))
+          .call(() => {
+            assertEquals(firstLessonNode, getRangeStartNode());
+            assertEquals(0, tutorial.activeLessonId);
+          })
+          .call(
+              simulateKeyPress.bind(this, KeyCode.LEFT, {searchKeyHeld: true}))
+          .call(() => {
+            assertEquals(firstLessonNode, getRangeStartNode());
+            assertEquals(0, tutorial.activeLessonId);
+          })
+          // Pressing space, which is the desired key sequence, should move us
+          // to the next lesson.
+          .call(simulateKeyPress.bind(this, KeyCode.SPACE, {}))
+          .expectSpeech('Essential Keys: Control')
+          .expectSpeech(/Let's start with a few keys you'll use regularly./)
+          .call(() => {
+            assertEquals(1, tutorial.activeLessonId);
+            assertNotEquals(firstLessonNode, getRangeStartNode());
+          })
+          // Pressing control, which is the desired key sequence, should move
+          // us to the next lesson.
+          .call(simulateKeyPress.bind(this, KeyCode.CONTROL, {}))
+          .expectSpeech('Essential Keys: Shift')
+          .call(() => {
+            assertEquals(2, tutorial.activeLessonId);
+          })
+          .replay();
     });
 
 // Tests that tutorial nudges are restarted whenever the current range changes.
-TEST_F('ChromeVoxTutorialTest', 'RestartNudges', function() {
-  this.runWithLoadedTree(this.simpleDoc, async function(root) {
-    await this.launchAndWaitForTutorial();
-    const tutorial = this.getTutorial();
-    let restart = false;
-    // Swap in below function to track when nudges get restarted.
-    tutorial.restartNudges = () => {
-      restart = true;
-    };
-    const waitForRestartNudges = async () => {
-      return new Promise(resolve => {
-        let intervalId;
-        const nudgesRestarted = () => {
-          return restart;
-        };
-        if (nudgesRestarted()) {
-          resolve();
-        } else {
-          intervalId = setInterval(() => {
-            if (nudgesRestarted()) {
-              clearInterval(intervalId);
-              resolve();
-            }
-          }, 500);
-        }
-      });
-    };
-    restart = false;
-    CommandHandlerInterface.instance.onCommand('nextObject');
-    await waitForRestartNudges();
-    // Show a lesson.
-    tutorial.curriculum = 'essential_keys';
-    tutorial.showLesson_(0);
-    restart = false;
-    CommandHandlerInterface.instance.onCommand('nextObject');
-    await waitForRestartNudges();
-    restart = false;
-    CommandHandlerInterface.instance.onCommand('nextObject');
-    await waitForRestartNudges();
-  });
+TEST_F('ChromeVoxTutorialTest', 'RestartNudges', async function() {
+  const root = await this.runWithLoadedTree(this.simpleDoc);
+  await this.launchAndWaitForTutorial();
+  const tutorial = this.getTutorial();
+  let restart = false;
+  // Swap in below function to track when nudges get restarted.
+  tutorial.restartNudges = () => {
+    restart = true;
+  };
+  const waitForRestartNudges = async () => {
+    return new Promise(resolve => {
+      let intervalId;
+      const nudgesRestarted = () => {
+        return restart;
+      };
+      if (nudgesRestarted()) {
+        resolve();
+      } else {
+        intervalId = setInterval(() => {
+          if (nudgesRestarted()) {
+            clearInterval(intervalId);
+            resolve();
+          }
+        }, 500);
+      }
+    });
+  };
+  restart = false;
+  CommandHandlerInterface.instance.onCommand('nextObject');
+  await waitForRestartNudges();
+  // Show a lesson.
+  tutorial.curriculum = 'essential_keys';
+  tutorial.showLesson_(0);
+  restart = false;
+  CommandHandlerInterface.instance.onCommand('nextObject');
+  await waitForRestartNudges();
+  restart = false;
+  CommandHandlerInterface.instance.onCommand('nextObject');
+  await waitForRestartNudges();
 });
 
 // Tests that the tutorial closes and ChromeVox navigates to a resource link.
-TEST_F('ChromeVoxTutorialTest', 'ResourcesTest', function() {
+TEST_F('ChromeVoxTutorialTest', 'ResourcesTest', async function() {
   const mockFeedback = this.createMockFeedback();
-  this.runWithLoadedTree(this.simpleDoc, async function(root) {
-    await this.launchAndWaitForTutorial();
-    const tutorial = this.getTutorial();
-    mockFeedback.expectSpeech('ChromeVox tutorial')
-        .call(() => {
-          tutorial.curriculum = 'resources';
-          tutorial.showLesson_(0);
-        })
-        .expectSpeech('Learn More')
-        .call(doCmd('nextObject'))
-        .expectSpeech(/Congratulations/)
-        .call(doCmd('nextObject'))
-        .expectSpeech('ChromeVox Command Reference', 'Link')
-        .call(doCmd('forceClickOnCurrentItem'))
-        .expectSpeech('support.google.com')
-        .replay();
-  });
+  const root = await this.runWithLoadedTree(this.simpleDoc);
+  await this.launchAndWaitForTutorial();
+  const tutorial = this.getTutorial();
+  mockFeedback.expectSpeech('ChromeVox tutorial')
+      .call(() => {
+        tutorial.curriculum = 'resources';
+        tutorial.showLesson_(0);
+      })
+      .expectSpeech('Learn More')
+      .call(doCmd('nextObject'))
+      .expectSpeech(/Congratulations/)
+      .call(doCmd('nextObject'))
+      .expectSpeech('ChromeVox Command Reference', 'Link')
+      .call(doCmd('forceClickOnCurrentItem'))
+      .expectSpeech('support.google.com')
+      .replay();
 });
 
 // Tests that choosing a curriculum with only 1 lesson automatically opens the
 // lesson.
-TEST_F('ChromeVoxTutorialTest', 'OnlyLessonTest', function() {
+TEST_F('ChromeVoxTutorialTest', 'OnlyLessonTest', async function() {
   const mockFeedback = this.createMockFeedback();
-  this.runWithLoadedTree(this.simpleDoc, async function(root) {
-    await this.launchAndWaitForTutorial();
-    const tutorial = this.getTutorial();
-    mockFeedback.expectSpeech('ChromeVox tutorial')
-        .call(doCmd('nextObject'))
-        .expectSpeech('Quick orientation')
-        .call(doCmd('nextObject'))
-        .expectSpeech('Essential keys')
-        .call(doCmd('nextObject'))
-        .expectSpeech('Navigation')
-        .call(doCmd('nextObject'))
-        .expectSpeech('Command references')
-        .call(doCmd('nextObject'))
-        .expectSpeech('Sounds and settings')
-        .call(doCmd('nextObject'))
-        .expectSpeech('Resources')
-        .call(doCmd('forceClickOnCurrentItem'))
-        .expectSpeech('Learn More', 'Heading 1')
-        .expectSpeech(
-            ' Press Search + Right Arrow, or Search + Left Arrow to' +
-            ' navigate this lesson ')
-        // The 'All lessons' button should be hidden since this is the only
-        // lesson for the curriculum.
-        .call(doCmd('nextButton'))
-        .expectSpeech('Main menu')
-        .call(doCmd('nextButton'))
-        .expectSpeech('Exit tutorial')
-        .replay();
-  });
+  const root = this.runWithLoadedTree(this.simpleDoc);
+  await this.launchAndWaitForTutorial();
+  const tutorial = this.getTutorial();
+  mockFeedback.expectSpeech('ChromeVox tutorial')
+      .call(doCmd('nextObject'))
+      .expectSpeech('Quick orientation')
+      .call(doCmd('nextObject'))
+      .expectSpeech('Essential keys')
+      .call(doCmd('nextObject'))
+      .expectSpeech('Navigation')
+      .call(doCmd('nextObject'))
+      .expectSpeech('Command references')
+      .call(doCmd('nextObject'))
+      .expectSpeech('Sounds and settings')
+      .call(doCmd('nextObject'))
+      .expectSpeech('Resources')
+      .call(doCmd('forceClickOnCurrentItem'))
+      .expectSpeech('Learn More', 'Heading 1')
+      .expectSpeech(
+          ' Press Search + Right Arrow, or Search + Left Arrow to' +
+          ' navigate this lesson ')
+      // The 'All lessons' button should be hidden since this is the only
+      // lesson for the curriculum.
+      .call(doCmd('nextButton'))
+      .expectSpeech('Main menu')
+      .call(doCmd('nextButton'))
+      .expectSpeech('Exit tutorial')
+      .replay();
 });
 
 // Tests that interactive mode and UserActionMonitor are properly set when
 // showing different screens in the tutorial.
-TEST_F('ChromeVoxTutorialTest', 'StartStopInteractiveMode', function() {
-  this.runWithLoadedTree(this.simpleDoc, async function(root) {
-    await this.launchAndWaitForTutorial();
-    const tutorial = this.getTutorial();
-    let userActionMonitorCreatedCount = 0;
-    let userActionMonitorDestroyedCount = 0;
-    let isUserActionMonitorActive = false;
+TEST_F('ChromeVoxTutorialTest', 'StartStopInteractiveMode', async function() {
+  const root = await this.runWithLoadedTree(this.simpleDoc);
+  await this.launchAndWaitForTutorial();
+  const tutorial = this.getTutorial();
+  let userActionMonitorCreatedCount = 0;
+  let userActionMonitorDestroyedCount = 0;
+  let isUserActionMonitorActive = false;
 
-    // Swap in functions below so we can track the number of times
-    // UserActionMonitor is created and destroyed.
-    ChromeVoxState.instance.createUserActionMonitor = (actions, callback) => {
-      userActionMonitorCreatedCount += 1;
-      isUserActionMonitorActive = true;
-    };
-    ChromeVoxState.instance.destroyUserActionMonitor = () => {
-      userActionMonitorDestroyedCount += 1;
-      isUserActionMonitorActive = false;
-    };
+  // Swap in functions below so we can track the number of times
+  // UserActionMonitor is created and destroyed.
+  ChromeVoxState.instance.createUserActionMonitor = (actions, callback) => {
+    userActionMonitorCreatedCount += 1;
+    isUserActionMonitorActive = true;
+  };
+  ChromeVoxState.instance.destroyUserActionMonitor = () => {
+    userActionMonitorDestroyedCount += 1;
+    isUserActionMonitorActive = false;
+  };
 
-    // A helper to make assertions on four variables of interest.
-    const makeAssertions = (expectedVars) => {
-      assertEquals(expectedVars.createdCount, userActionMonitorCreatedCount);
-      assertEquals(
-          expectedVars.destroyedCount, userActionMonitorDestroyedCount);
-      assertEquals(expectedVars.interactiveMode, tutorial.interactiveMode_);
-      // Note: Interactive mode and UserActionMonitor should always be in
-      // sync in the context of the tutorial.
-      assertEquals(expectedVars.interactiveMode, isUserActionMonitorActive);
-    };
+  // A helper to make assertions on four variables of interest.
+  const makeAssertions = (expectedVars) => {
+    assertEquals(expectedVars.createdCount, userActionMonitorCreatedCount);
+    assertEquals(expectedVars.destroyedCount, userActionMonitorDestroyedCount);
+    assertEquals(expectedVars.interactiveMode, tutorial.interactiveMode_);
+    // Note: Interactive mode and UserActionMonitor should always be in
+    // sync in the context of the tutorial.
+    assertEquals(expectedVars.interactiveMode, isUserActionMonitorActive);
+  };
 
-    makeAssertions(
-        {createdCount: 0, destroyedCount: 0, interactiveMode: false});
-    // Show the first lesson of the quick orientation, which is interactive.
-    tutorial.curriculum = 'quick_orientation';
-    tutorial.showLesson_(0);
-    makeAssertions({createdCount: 1, destroyedCount: 0, interactiveMode: true});
+  makeAssertions({createdCount: 0, destroyedCount: 0, interactiveMode: false});
+  // Show the first lesson of the quick orientation, which is interactive.
+  tutorial.curriculum = 'quick_orientation';
+  tutorial.showLesson_(0);
+  makeAssertions({createdCount: 1, destroyedCount: 0, interactiveMode: true});
 
-    // Move to the next lesson in the quick orientation. This lesson is also
-    // interactive, so UserActionMonitor should be destroyed and re-created.
-    tutorial.showNextLesson();
-    makeAssertions({createdCount: 2, destroyedCount: 1, interactiveMode: true});
+  // Move to the next lesson in the quick orientation. This lesson is also
+  // interactive, so UserActionMonitor should be destroyed and re-created.
+  tutorial.showNextLesson();
+  makeAssertions({createdCount: 2, destroyedCount: 1, interactiveMode: true});
 
-    // Leave the quick orientation by navigating to the lesson menu. This should
-    // stop interactive mode and destroy UserActionMonitor.
-    tutorial.showLessonMenu_();
-    makeAssertions(
-        {createdCount: 2, destroyedCount: 2, interactiveMode: false});
-  });
+  // Leave the quick orientation by navigating to the lesson menu. This should
+  // stop interactive mode and destroy UserActionMonitor.
+  tutorial.showLessonMenu_();
+  makeAssertions({createdCount: 2, destroyedCount: 2, interactiveMode: false});
 });
 
 // Tests that gestures can be used in the tutorial to navigate.
-TEST_F('ChromeVoxTutorialTest', 'Gestures', function() {
+TEST_F('ChromeVoxTutorialTest', 'Gestures', async function() {
   const mockFeedback = this.createMockFeedback();
-  this.runWithLoadedTree(this.simpleDoc, async function(root) {
-    await this.launchAndWaitForTutorial();
-    const tutorial = this.getTutorial();
-    mockFeedback.expectSpeech('ChromeVox tutorial')
-        .call(doGesture(Gesture.SWIPE_RIGHT1))
-        .expectSpeech('Quick orientation', 'Link')
-        .call(doGesture(Gesture.SWIPE_RIGHT1))
-        .expectSpeech('Essential keys', 'Link')
-        .call(doGesture(Gesture.SWIPE_LEFT1))
-        .expectSpeech('Quick orientation', 'Link')
-        .call(doGesture(Gesture.SWIPE_LEFT2))
-        .expectSpeech('Some web content')
-        .replay();
-  });
+  const root = await this.runWithLoadedTree(this.simpleDoc);
+  await this.launchAndWaitForTutorial();
+  const tutorial = this.getTutorial();
+  mockFeedback.expectSpeech('ChromeVox tutorial')
+      .call(doGesture(Gesture.SWIPE_RIGHT1))
+      .expectSpeech('Quick orientation', 'Link')
+      .call(doGesture(Gesture.SWIPE_RIGHT1))
+      .expectSpeech('Essential keys', 'Link')
+      .call(doGesture(Gesture.SWIPE_LEFT1))
+      .expectSpeech('Quick orientation', 'Link')
+      .call(doGesture(Gesture.SWIPE_LEFT2))
+      .expectSpeech('Some web content')
+      .replay();
 });
 
 // Tests that touch orientation loads properly. Tests string content, but does
 // not test interactivity of lessons.
 // TODO(crbug.com/1193799): fix ax node errors causing console spew and
 // breaking tests
-TEST_F('ChromeVoxTutorialTest', 'DISABLED_TouchOrientation', function() {
+TEST_F('ChromeVoxTutorialTest', 'DISABLED_TouchOrientation', async function() {
   const mockFeedback = this.createMockFeedback();
-  this.runWithLoadedTree(this.simpleDoc, async function(root) {
-    await this.launchAndWaitForTutorial();
-    const tutorial = this.getTutorial();
-    mockFeedback.expectSpeech('ChromeVox tutorial')
-        .call(() => {
-          tutorial.curriculum = 'touch_orientation';
-          tutorial.medium = 'touch';
-          tutorial.showLesson_(0);
-          this.assertActiveLessonIndex(0);
-          this.assertActiveScreen('lesson');
-        })
-        .expectSpeech('ChromeVox touch tutorial')
-        .expectSpeech(/Welcome to the ChromeVox tutorial/)
-        .call(doGesture(Gesture.CLICK))
-        .expectSpeech('Activate an item')
-        .expectSpeech(/To continue, double-tap now/)
-        .call(doGesture(Gesture.CLICK))
-        .expectSpeech('Move to the next or previous item')
-        .call(() => {
-          // Jump to the penultimate lesson.
-          tutorial.showLesson_(6);
-        })
-        .expectSpeech('Move to the next or previous section')
-        .expectSpeech(/swipe from left to right with four fingers/)
-        .call(doGesture(Gesture.SWIPE_RIGHT4))
-        .expectSpeech(/swiping with four fingers from right to left/)
-        .call(doGesture(Gesture.SWIPE_LEFT4))
-        .expectSpeech('Touch tutorial complete')
-        .replay();
-  });
+  const root = await this.runWithLoadedTree(this.simpleDoc);
+  await this.launchAndWaitForTutorial();
+  const tutorial = this.getTutorial();
+  mockFeedback.expectSpeech('ChromeVox tutorial')
+      .call(() => {
+        tutorial.curriculum = 'touch_orientation';
+        tutorial.medium = 'touch';
+        tutorial.showLesson_(0);
+        this.assertActiveLessonIndex(0);
+        this.assertActiveScreen('lesson');
+      })
+      .expectSpeech('ChromeVox touch tutorial')
+      .expectSpeech(/Welcome to the ChromeVox tutorial/)
+      .call(doGesture(Gesture.CLICK))
+      .expectSpeech('Activate an item')
+      .expectSpeech(/To continue, double-tap now/)
+      .call(doGesture(Gesture.CLICK))
+      .expectSpeech('Move to the next or previous item')
+      .call(() => {
+        // Jump to the penultimate lesson.
+        tutorial.showLesson_(6);
+      })
+      .expectSpeech('Move to the next or previous section')
+      .expectSpeech(/swipe from left to right with four fingers/)
+      .call(doGesture(Gesture.SWIPE_RIGHT4))
+      .expectSpeech(/swiping with four fingers from right to left/)
+      .call(doGesture(Gesture.SWIPE_LEFT4))
+      .expectSpeech('Touch tutorial complete')
+      .replay();
 });
 
-TEST_F('ChromeVoxTutorialTest', 'GeneralTouchNudges', function() {
+TEST_F('ChromeVoxTutorialTest', 'GeneralTouchNudges', async function() {
   const mockFeedback = this.createMockFeedback();
-  this.runWithLoadedTree(this.simpleDoc, async function(root) {
-    await this.launchAndWaitForTutorial();
-    const tutorial = this.getTutorial();
-    const giveNudge = () => {
-      tutorial.giveNudge();
-    };
-    mockFeedback.expectSpeech('ChromeVox tutorial');
-    mockFeedback.call(() => {
-      tutorial.medium = 'touch';
-      tutorial.initializeNudges('general');
-    });
-    for (let i = 0; i < 3; ++i) {
-      mockFeedback.call(giveNudge).expectSpeech(
-          'ChromeVox tutorial', 'Heading 1');
-    }
-    mockFeedback.call(giveNudge)
-        .expectSpeech('Hint: Swipe left or right with one finger to navigate.')
-        .call(giveNudge)
-        .expectSpeech(
-            'Hint: Double-tap with one finger to activate the current item.')
-        .call(giveNudge)
-        .expectSpeech(
-            'Hint: Swipe from right to left with two fingers if you would ' +
-            'like to exit this tutorial.')
-        .replay();
+  const root = await this.runWithLoadedTree(this.simpleDoc);
+  await this.launchAndWaitForTutorial();
+  const tutorial = this.getTutorial();
+  const giveNudge = () => {
+    tutorial.giveNudge();
+  };
+  mockFeedback.expectSpeech('ChromeVox tutorial');
+  mockFeedback.call(() => {
+    tutorial.medium = 'touch';
+    tutorial.initializeNudges('general');
   });
+  for (let i = 0; i < 3; ++i) {
+    mockFeedback.call(giveNudge).expectSpeech(
+        'ChromeVox tutorial', 'Heading 1');
+  }
+  mockFeedback.call(giveNudge)
+      .expectSpeech('Hint: Swipe left or right with one finger to navigate.')
+      .call(giveNudge)
+      .expectSpeech(
+          'Hint: Double-tap with one finger to activate the current item.')
+      .call(giveNudge)
+      .expectSpeech(
+          'Hint: Swipe from right to left with two fingers if you would ' +
+          'like to exit this tutorial.')
+      .replay();
 });
diff --git a/chrome/browser/resources/chromeos/accessibility/chromevox/testing/chromevox_next_e2e_test_base.js b/chrome/browser/resources/chromeos/accessibility/chromevox/testing/chromevox_next_e2e_test_base.js
index 9c133e1..a7bb66574 100644
--- a/chrome/browser/resources/chromeos/accessibility/chromevox/testing/chromevox_next_e2e_test_base.js
+++ b/chrome/browser/resources/chromeos/accessibility/chromevox/testing/chromevox_next_e2e_test_base.js
@@ -46,8 +46,7 @@
 
   /** @return {!MockFeedback} */
   createMockFeedback() {
-    const mockFeedback =
-        new MockFeedback(this.newCallback(), this.newCallback.bind(this));
+    const mockFeedback = new MockFeedback(this.newCallback());
     mockFeedback.install();
     return mockFeedback;
   }
@@ -126,14 +125,10 @@
   }
 
   /** @override */
-  runWithLoadedTree(doc, callback, opt_params = {}) {
-    callback = this.newCallback(callback);
-    const wrappedCallback = (node) => {
-      CommandHandlerInterface.instance.onCommand('nextObject');
-      callback(node);
-    };
-
-    super.runWithLoadedTree(doc, wrappedCallback, opt_params);
+  async runWithLoadedTree(doc, opt_params = {}) {
+    const rootWebArea = await super.runWithLoadedTree(doc, opt_params);
+    CommandHandlerInterface.instance.onCommand('nextObject');
+    return rootWebArea;
   }
 
   /**
diff --git a/chrome/browser/resources/chromeos/accessibility/common/array_util_test.js b/chrome/browser/resources/chromeos/accessibility/common/array_util_test.js
index f642c707..5d93f255 100644
--- a/chrome/browser/resources/chromeos/accessibility/common/array_util_test.js
+++ b/chrome/browser/resources/chromeos/accessibility/common/array_util_test.js
@@ -9,42 +9,40 @@
 /** Test fixture for array_util.js. */
 ArrayUtilTest = class extends ChromeVoxNextE2ETest {};
 
-TEST_F('ArrayUtilTest', 'ContentsAreEqual', function() {
-  this.runWithLoadedTree('', (root) => {
-    const even1 = [2, 4, 6, 8];
-    const even2 = [2, 4, 6, 8];
-    const odd = [1, 3, 5, 7, 9];
-    const powersOf2 = [2, 4, 8, 16];
+SYNC_TEST_F('ArrayUtilTest', 'ContentsAreEqual', function() {
+  const even1 = [2, 4, 6, 8];
+  const even2 = [2, 4, 6, 8];
+  const odd = [1, 3, 5, 7, 9];
+  const powersOf2 = [2, 4, 8, 16];
 
-    assertFalse(
-        ArrayUtil.contentsAreEqual(even1, odd),
-        'Arrays with different lengths should not be equal.');
-    assertFalse(
-        ArrayUtil.contentsAreEqual(even1, powersOf2),
-        'Arrays with some common elements should not be equal.');
-    assertTrue(
-        ArrayUtil.contentsAreEqual(even1, even1),
-        'Arrays should equal themselves.');
-    assertTrue(
-        ArrayUtil.contentsAreEqual(even1, even2),
-        'Two different array objects with the same elements should be equal.');
+  assertFalse(
+      ArrayUtil.contentsAreEqual(even1, odd),
+      'Arrays with different lengths should not be equal.');
+  assertFalse(
+      ArrayUtil.contentsAreEqual(even1, powersOf2),
+      'Arrays with some common elements should not be equal.');
+  assertTrue(
+      ArrayUtil.contentsAreEqual(even1, even1),
+      'Arrays should equal themselves.');
+  assertTrue(
+      ArrayUtil.contentsAreEqual(even1, even2),
+      'Two different array objects with the same elements should be equal.');
 
-    const obj = {};
-    const arrayWithObj = [obj];
-    const secondArrayWithObj = [obj];
-    const arrayWithDifferentObj = [{}];
+  const obj = {};
+  const arrayWithObj = [obj];
+  const secondArrayWithObj = [obj];
+  const arrayWithDifferentObj = [{}];
 
-    assertNotEquals(
-        arrayWithObj, secondArrayWithObj,
-        'Different array instances with the same contents should not be ' +
-            'equal with ===.');
-    assertTrue(
-        ArrayUtil.contentsAreEqual(arrayWithObj, secondArrayWithObj),
-        'Different array instances with references to the same object ' +
-            'instance should be equal with contentsAreEqual.');
-    assertFalse(
-        ArrayUtil.contentsAreEqual(arrayWithObj, arrayWithDifferentObj),
-        'Arrays with different objects should not be equal (ArrayUtil.' +
-            'contentsAreEqual uses shallow equality for the elements).');
-  });
+  assertNotEquals(
+      arrayWithObj, secondArrayWithObj,
+      'Different array instances with the same contents should not be ' +
+          'equal with ===.');
+  assertTrue(
+      ArrayUtil.contentsAreEqual(arrayWithObj, secondArrayWithObj),
+      'Different array instances with references to the same object ' +
+          'instance should be equal with contentsAreEqual.');
+  assertFalse(
+      ArrayUtil.contentsAreEqual(arrayWithObj, arrayWithDifferentObj),
+      'Arrays with different objects should not be equal (ArrayUtil.' +
+          'contentsAreEqual uses shallow equality for the elements).');
 });
diff --git a/chrome/browser/resources/chromeos/accessibility/common/automation_predicate_test.js b/chrome/browser/resources/chromeos/accessibility/common/automation_predicate_test.js
index 7cc5dc27..e3d0449 100644
--- a/chrome/browser/resources/chromeos/accessibility/common/automation_predicate_test.js
+++ b/chrome/browser/resources/chromeos/accessibility/common/automation_predicate_test.js
@@ -8,34 +8,33 @@
 /** Test fixture for automation_predicate.js. */
 AutomationPredicateTest = class extends ChromeVoxNextE2ETest {};
 
-TEST_F('AutomationPredicateTest', 'EquivalentRoles', function() {
+TEST_F('AutomationPredicateTest', 'EquivalentRoles', async function() {
   const site = `
     <input type="text"></input>
     <input role="combobox"></input>
   `;
-  this.runWithLoadedTree(site, (root) => {
-    // Text field is equivalent to text field with combo box.
-    const textField = root.find({role: RoleType.TEXT_FIELD});
-    assertTrue(!!textField, 'No text field found.');
-    const textFieldWithComboBox =
-        root.find({role: RoleType.TEXT_FIELD_WITH_COMBO_BOX});
-    assertTrue(!!textFieldWithComboBox, 'No text field with combo box found.');
+  const root = await this.runWithLoadedTree(site);
+  // Text field is equivalent to text field with combo box.
+  const textField = root.find({role: RoleType.TEXT_FIELD});
+  assertTrue(!!textField, 'No text field found.');
+  const textFieldWithComboBox =
+      root.find({role: RoleType.TEXT_FIELD_WITH_COMBO_BOX});
+  assertTrue(!!textFieldWithComboBox, 'No text field with combo box found.');
 
-    // Gather all potential predicate names.
-    const keys = Object.getOwnPropertyNames(AutomationPredicate);
-    for (const key of keys) {
-      // Not all keys are functions or predicates e.g. makeTableCellPredicate.
-      if (typeof (AutomationPredicate[key]) !== 'function' ||
-          key.indexOf('make') === 0) {
-        continue;
-      }
-
-      const predicate = AutomationPredicate[key];
-      if (predicate(textField)) {
-        assertTrue(
-            !!predicate(textFieldWithComboBox),
-            `Textfield with combo box should match predicate ${key}`);
-      }
+  // Gather all potential predicate names.
+  const keys = Object.getOwnPropertyNames(AutomationPredicate);
+  for (const key of keys) {
+    // Not all keys are functions or predicates e.g. makeTableCellPredicate.
+    if (typeof (AutomationPredicate[key]) !== 'function' ||
+        key.indexOf('make') === 0) {
+      continue;
     }
-  });
+
+    const predicate = AutomationPredicate[key];
+    if (predicate(textField)) {
+      assertTrue(
+          !!predicate(textFieldWithComboBox),
+          `Textfield with combo box should match predicate ${key}`);
+    }
+  }
 });
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 8d56b867..c20602f 100644
--- a/chrome/browser/resources/chromeos/accessibility/common/automation_util_test.js
+++ b/chrome/browser/resources/chromeos/accessibility/common/automation_util_test.js
@@ -64,206 +64,180 @@
 
 
 TEST_F(
-    'AccessibilityExtensionAutomationUtilE2ETest', 'GetAncestors', function() {
-      this.runWithLoadedTree(this.basicDoc(), function(root) {
-        let expectedLength = 1;
-        while (root) {
-          const ancestors = getNonDesktopAncestors(root);
-          assertEquals(expectedLength++, ancestors.length);
-          root = root.firstChild;
-        }
-      });
+    'AccessibilityExtensionAutomationUtilE2ETest', 'GetAncestors',
+    async function() {
+      const root = await this.runWithLoadedTree(this.basicDoc());
+      let expectedLength = 1;
+      while (root) {
+        const ancestors = getNonDesktopAncestors(root);
+        assertEquals(expectedLength++, ancestors.length);
+        root = root.firstChild;
+      }
     });
 
 TEST_F(
     'AccessibilityExtensionAutomationUtilE2ETest', 'GetFirstAncestorWithRole',
-    function() {
-      this.runWithLoadedTree(
-          `
+    async function() {
+      const root = await this.runWithLoadedTree(`
     <div tabindex="0" aria-label="x">
       <div tabindex="0" aria-label="y">
         <p>
           <button>Hello world</div>
         </p>
       </div>
-    </div>`,
-          function(root) {
-            const buttonNode = root.firstChild.firstChild.firstChild;
-            const containerNode = AutomationUtil.getFirstAncestorWithRole(
-                buttonNode, RoleType.GENERIC_CONTAINER);
-            assertEquals(containerNode.name, 'y');
+    </div>`);
+      const buttonNode = root.firstChild.firstChild.firstChild;
+      const containerNode = AutomationUtil.getFirstAncestorWithRole(
+          buttonNode, RoleType.GENERIC_CONTAINER);
+      assertEquals(containerNode.name, 'y');
 
-            const parentContainerNode = AutomationUtil.getFirstAncestorWithRole(
-                containerNode, RoleType.GENERIC_CONTAINER);
-            assertEquals(parentContainerNode.name, 'x');
-          });
+      const parentContainerNode = AutomationUtil.getFirstAncestorWithRole(
+          containerNode, RoleType.GENERIC_CONTAINER);
+      assertEquals(parentContainerNode.name, 'x');
     });
 
 TEST_F(
     'AccessibilityExtensionAutomationUtilE2ETest', 'GetUniqueAncestors',
-    function() {
-      this.runWithLoadedTree(this.basicDoc(), function(root) {
-        let leftmost = root, rightmost = root;
-        while (leftmost.firstChild) {
-          leftmost = leftmost.firstChild;
-        }
-        while (rightmost.lastChild) {
-          rightmost = rightmost.lastChild;
-        }
+    async function() {
+      const root = await this.runWithLoadedTree(this.basicDoc());
+      let leftmost = root, rightmost = root;
+      while (leftmost.firstChild) {
+        leftmost = leftmost.firstChild;
+      }
+      while (rightmost.lastChild) {
+        rightmost = rightmost.lastChild;
+      }
 
-        const leftAncestors = getNonDesktopAncestors(leftmost);
-        const rightAncestors = getNonDesktopAncestors(rightmost);
-        assertEquals(RoleType.LINK, leftmost.role);
-        assertEquals(RoleType.BUTTON, rightmost.role);
-        assertEquals(
-            1, AutomationUtil.getDivergence(leftAncestors, rightAncestors));
+      const leftAncestors = getNonDesktopAncestors(leftmost);
+      const rightAncestors = getNonDesktopAncestors(rightmost);
+      assertEquals(RoleType.LINK, leftmost.role);
+      assertEquals(RoleType.BUTTON, rightmost.role);
+      assertEquals(
+          1, AutomationUtil.getDivergence(leftAncestors, rightAncestors));
 
-        assertEquals(
-            -1, AutomationUtil.getDivergence(leftAncestors, leftAncestors));
+      assertEquals(
+          -1, AutomationUtil.getDivergence(leftAncestors, leftAncestors));
 
-        const uniqueAncestorsLeft =
-            getNonDesktopUniqueAncestors(rightmost, leftmost);
-        const uniqueAncestorsRight =
-            getNonDesktopUniqueAncestors(leftmost, rightmost);
+      const uniqueAncestorsLeft =
+          getNonDesktopUniqueAncestors(rightmost, leftmost);
+      const uniqueAncestorsRight =
+          getNonDesktopUniqueAncestors(leftmost, rightmost);
 
-        assertEquals(2, uniqueAncestorsLeft.length);
-        assertEquals(RoleType.PARAGRAPH, uniqueAncestorsLeft[0].role);
-        assertEquals(RoleType.LINK, uniqueAncestorsLeft[1].role);
+      assertEquals(2, uniqueAncestorsLeft.length);
+      assertEquals(RoleType.PARAGRAPH, uniqueAncestorsLeft[0].role);
+      assertEquals(RoleType.LINK, uniqueAncestorsLeft[1].role);
 
-        assertEquals(3, uniqueAncestorsRight.length);
-        assertEquals(RoleType.HEADING, uniqueAncestorsRight[0].role);
-        assertEquals(RoleType.GROUP, uniqueAncestorsRight[1].role);
-        assertEquals(RoleType.BUTTON, uniqueAncestorsRight[2].role);
+      assertEquals(3, uniqueAncestorsRight.length);
+      assertEquals(RoleType.HEADING, uniqueAncestorsRight[0].role);
+      assertEquals(RoleType.GROUP, uniqueAncestorsRight[1].role);
+      assertEquals(RoleType.BUTTON, uniqueAncestorsRight[2].role);
 
-        assertEquals(
-            1, getNonDesktopUniqueAncestors(leftmost, leftmost).length);
-      }.bind(this));
+      assertEquals(1, getNonDesktopUniqueAncestors(leftmost, leftmost).length);
     });
 
 TEST_F(
-    'AccessibilityExtensionAutomationUtilE2ETest', 'GetDirection', function() {
-      this.runWithLoadedTree(this.basicDoc(), function(root) {
-        let left = root, right = root;
+    'AccessibilityExtensionAutomationUtilE2ETest', 'GetDirection',
+    async function() {
+      const root = await this.runWithLoadedTree(this.basicDoc());
+      let left = root, right = root;
 
-        // Same node.
-        assertEquals(Dir.FORWARD, AutomationUtil.getDirection(left, right));
+      // Same node.
+      assertEquals(Dir.FORWARD, AutomationUtil.getDirection(left, right));
 
-        // Ancestry.
-        left = left.firstChild;
-        // Upward movement is backward (in dfs).
-        assertEquals(Dir.BACKWARD, AutomationUtil.getDirection(left, right));
-        // Downward movement is forward.
-        assertEquals(Dir.FORWARD, AutomationUtil.getDirection(right, left));
+      // Ancestry.
+      left = left.firstChild;
+      // Upward movement is backward (in dfs).
+      assertEquals(Dir.BACKWARD, AutomationUtil.getDirection(left, right));
+      // Downward movement is forward.
+      assertEquals(Dir.FORWARD, AutomationUtil.getDirection(right, left));
 
-        // Ordered.
-        right = right.lastChild;
-        assertEquals(Dir.BACKWARD, AutomationUtil.getDirection(right, left));
-        assertEquals(Dir.FORWARD, AutomationUtil.getDirection(left, right));
-      });
+      // Ordered.
+      right = right.lastChild;
+      assertEquals(Dir.BACKWARD, AutomationUtil.getDirection(right, left));
+      assertEquals(Dir.FORWARD, AutomationUtil.getDirection(left, right));
     });
 
 TEST_F(
     'AccessibilityExtensionAutomationUtilE2ETest', 'VisitContainer',
-    function() {
-      this.runWithLoadedTree(toolbarDoc(), function(r) {
-        const pred = function(n) {
-          return n.role !== 'rootWebArea';
-        };
+    async function() {
+      const r = await this.runWithLoadedTree(toolbarDoc());
+      const pred = function(n) {
+        return n.role !== 'rootWebArea';
+      };
 
-        const toolbar = AutomationUtil.findNextNode(r, 'forward', pred);
-        assertEquals('toolbar', toolbar.role);
+      const toolbar = AutomationUtil.findNextNode(r, 'forward', pred);
+      assertEquals('toolbar', toolbar.role);
 
-        const back = AutomationUtil.findNextNode(toolbar, 'forward', pred);
-        assertEquals('Back', back.name);
-        assertEquals(
-            toolbar, AutomationUtil.findNextNode(back, 'backward', pred));
+      const back = AutomationUtil.findNextNode(toolbar, 'forward', pred);
+      assertEquals('Back', back.name);
+      assertEquals(
+          toolbar, AutomationUtil.findNextNode(back, 'backward', pred));
 
-        const forward = AutomationUtil.findNextNode(back, 'forward', pred);
-        assertEquals('Forward', forward.name);
-        assertEquals(
-            back, AutomationUtil.findNextNode(forward, 'backward', pred));
-      });
+      const forward = AutomationUtil.findNextNode(back, 'forward', pred);
+      assertEquals('Forward', forward.name);
+      assertEquals(
+          back, AutomationUtil.findNextNode(forward, 'backward', pred));
     });
 
-TEST_F('AccessibilityExtensionAutomationUtilE2ETest', 'HitTest', function() {
-  this.runWithLoadedTree(headingDoc, function(r) {
-    const [h1, h2, a] = r.findAll({role: 'inlineTextBox'});
+TEST_F(
+    'AccessibilityExtensionAutomationUtilE2ETest', 'HitTest', async function() {
+      const r = await this.runWithLoadedTree(headingDoc);
+      const [h1, h2, a] = r.findAll({role: 'inlineTextBox'});
 
-    assertEquals(h1, AutomationUtil.hitTest(r, RectUtil.center(h1.location)));
-    assertEquals(
-        h1, AutomationUtil.hitTest(r, RectUtil.center(h1.parent.location)));
-    assertEquals(
-        h1.parent.parent,
-        AutomationUtil.hitTest(r, RectUtil.center(h1.parent.parent.location)));
+      assertEquals(h1, AutomationUtil.hitTest(r, RectUtil.center(h1.location)));
+      assertEquals(
+          h1, AutomationUtil.hitTest(r, RectUtil.center(h1.parent.location)));
+      assertEquals(
+          h1.parent.parent,
+          AutomationUtil.hitTest(
+              r, RectUtil.center(h1.parent.parent.location)));
 
-    assertEquals(a, AutomationUtil.hitTest(r, RectUtil.center(a.location)));
-    assertEquals(
-        a, AutomationUtil.hitTest(r, RectUtil.center(a.parent.location)));
-    assertEquals(
-        a.parent.parent,
-        AutomationUtil.hitTest(r, RectUtil.center(a.parent.parent.location)));
-  });
-});
+      assertEquals(a, AutomationUtil.hitTest(r, RectUtil.center(a.location)));
+      assertEquals(
+          a, AutomationUtil.hitTest(r, RectUtil.center(a.parent.location)));
+      assertEquals(
+          a.parent.parent,
+          AutomationUtil.hitTest(r, RectUtil.center(a.parent.parent.location)));
+    });
 
 TEST_F(
     'AccessibilityExtensionAutomationUtilE2ETest', 'FindLastNodeSimple',
-    function() {
-      this.runWithLoadedTree(
-          `<p aria-label=" "><div tabindex="0" aria-label="x"></div></p>`,
-          function(r) {
-            assertEquals(
-                'x',
-                AutomationUtil
-                    .findLastNode(
-                        r,
-                        function(n) {
-                          return n.role === RoleType.GENERIC_CONTAINER;
-                        })
-                    .name);
-          });
+    async function() {
+      const r = await this.runWithLoadedTree(
+          `<p aria-label=" "><div tabindex="0" aria-label="x"></div></p>`);
+      assertEquals(
+          'x',
+          AutomationUtil
+              .findLastNode(r, (n) => n.role === RoleType.GENERIC_CONTAINER)
+              .name);
     });
 
 TEST_F(
     'AccessibilityExtensionAutomationUtilE2ETest', 'FindLastNodeNonLeaf',
-    function() {
-      this.runWithLoadedTree(
-          `
+    async function() {
+      const r = await this.runWithLoadedTree(`
     <div role="button" aria-label="outer">
       <div role="heading" aria-label="inner">
       </div>
     </div>
-    `,
-          function(r) {
-            assertEquals(
-                'outer',
-                AutomationUtil
-                    .findLastNode(
-                        r,
-                        function(n) {
-                          return n.role === RoleType.BUTTON;
-                        })
-                    .name);
-          });
+    `);
+      assertEquals(
+          'outer',
+          AutomationUtil.findLastNode(r, (n) => n.role === RoleType.BUTTON)
+              .name);
     });
 
 TEST_F(
     'AccessibilityExtensionAutomationUtilE2ETest', 'FindLastNodeLeaf',
-    function() {
-      this.runWithLoadedTree(
-          `
+    async function() {
+      const r = await this.runWithLoadedTree(`
     <p>start</p>
     <div aria-label="outer"><div tabindex="0" aria-label="inner"></div></div>
     <p>end</p>
-    `,
-          function(r) {
-            assertEquals(
-                'inner',
-                AutomationUtil
-                    .findLastNode(
-                        r,
-                        function(n) {
-                          return n.role === RoleType.GENERIC_CONTAINER;
-                        })
-                    .name);
-          });
+    `);
+      assertEquals(
+          'inner',
+          AutomationUtil
+              .findLastNode(r, (n) => n.role === RoleType.GENERIC_CONTAINER)
+              .name);
     });
diff --git a/chrome/browser/resources/chromeos/accessibility/common/cursors/cursors_test.js b/chrome/browser/resources/chromeos/accessibility/common/cursors/cursors_test.js
index a7e983f6..ffbf3af 100644
--- a/chrome/browser/resources/chromeos/accessibility/common/cursors/cursors_test.js
+++ b/chrome/browser/resources/chromeos/accessibility/common/cursors/cursors_test.js
@@ -105,23 +105,22 @@
    *     |rangeMoveAndAssert|.
    * @param {string=} opt_testType Either CURSOR or RANGE.
    */
-  runCursorMovesOnDocument(doc, moves, opt_testType) {
-    this.runWithLoadedTree(doc, function(root) {
-      let start = null;
+  async runCursorMovesOnDocument(doc, moves, opt_testType) {
+    const root = await this.runWithLoadedTree(doc);
+    let start = null;
 
-      // This occurs as a result of a load complete.
-      start =
-          AutomationUtil.findNodePost(root, FORWARD, AutomationPredicate.leaf);
+    // This occurs as a result of a load complete.
+    start =
+        AutomationUtil.findNodePost(root, FORWARD, AutomationPredicate.leaf);
 
+    const cursor = new cursors.Cursor(start, 0);
+    if (!opt_testType || opt_testType === this.CURSOR) {
       const cursor = new cursors.Cursor(start, 0);
-      if (!opt_testType || opt_testType === this.CURSOR) {
-        const cursor = new cursors.Cursor(start, 0);
-        this.cursorMoveAndAssert(cursor, moves);
-      } else if (opt_testType === this.RANGE) {
-        const range = new cursors.Range(cursor, cursor);
-        this.rangeMoveAndAssert(range, moves);
-      }
-    });
+      this.cursorMoveAndAssert(cursor, moves);
+    } else if (opt_testType === this.RANGE) {
+      const range = new cursors.Range(cursor, cursor);
+      this.rangeMoveAndAssert(range, moves);
+    }
   }
 
   get simpleDoc() {
@@ -147,29 +146,30 @@
 };
 
 
-TEST_F('AccessibilityExtensionCursorsTest', 'CharacterCursor', function() {
-  this.runCursorMovesOnDocument(this.simpleDoc, [
-    [CHARACTER, DIRECTIONAL, FORWARD, {index: 1, value: 'start '}],
-    [CHARACTER, DIRECTIONAL, BACKWARD, {index: 0, value: 'start '}],
-    // Bumps up against edge.
-    [CHARACTER, DIRECTIONAL, BACKWARD, {index: 0, value: 'start '}],
+TEST_F(
+    'AccessibilityExtensionCursorsTest', 'CharacterCursor', async function() {
+      await this.runCursorMovesOnDocument(this.simpleDoc, [
+        [CHARACTER, DIRECTIONAL, FORWARD, {index: 1, value: 'start '}],
+        [CHARACTER, DIRECTIONAL, BACKWARD, {index: 0, value: 'start '}],
+        // Bumps up against edge.
+        [CHARACTER, DIRECTIONAL, BACKWARD, {index: 0, value: 'start '}],
 
-    [CHARACTER, DIRECTIONAL, FORWARD, {index: 1, value: 'start '}],
-    [CHARACTER, DIRECTIONAL, FORWARD, {index: 2, value: 'start '}],
-    [CHARACTER, DIRECTIONAL, FORWARD, {index: 3, value: 'start '}],
-    [CHARACTER, DIRECTIONAL, FORWARD, {index: 4, value: 'start '}],
-    [CHARACTER, DIRECTIONAL, FORWARD, {index: 5, value: 'start '}],
+        [CHARACTER, DIRECTIONAL, FORWARD, {index: 1, value: 'start '}],
+        [CHARACTER, DIRECTIONAL, FORWARD, {index: 2, value: 'start '}],
+        [CHARACTER, DIRECTIONAL, FORWARD, {index: 3, value: 'start '}],
+        [CHARACTER, DIRECTIONAL, FORWARD, {index: 4, value: 'start '}],
+        [CHARACTER, DIRECTIONAL, FORWARD, {index: 5, value: 'start '}],
 
-    [CHARACTER, DIRECTIONAL, FORWARD, {index: 0, value: 'same line'}],
-    [CHARACTER, DIRECTIONAL, FORWARD, {index: 1, value: 'same line'}],
-    [CHARACTER, DIRECTIONAL, BACKWARD, {index: 0, value: 'same line'}],
+        [CHARACTER, DIRECTIONAL, FORWARD, {index: 0, value: 'same line'}],
+        [CHARACTER, DIRECTIONAL, FORWARD, {index: 1, value: 'same line'}],
+        [CHARACTER, DIRECTIONAL, BACKWARD, {index: 0, value: 'same line'}],
 
-    [CHARACTER, DIRECTIONAL, BACKWARD, {index: 5, value: 'start '}],
-  ]);
-});
+        [CHARACTER, DIRECTIONAL, BACKWARD, {index: 5, value: 'start '}],
+      ]);
+    });
 
-TEST_F('AccessibilityExtensionCursorsTest', 'WordCursor', function() {
-  this.runCursorMovesOnDocument(this.simpleDoc, [
+TEST_F('AccessibilityExtensionCursorsTest', 'WordCursor', async function() {
+  await this.runCursorMovesOnDocument(this.simpleDoc, [
     // Word (BOUND).
     [WORD, BOUND, BACKWARD, {index: 0, value: 'start '}],
     [WORD, BOUND, BACKWARD, {index: 0, value: 'start '}],
@@ -191,25 +191,27 @@
   ]);
 });
 
-TEST_F('AccessibilityExtensionCursorsTest', 'CharacterWordCursor', function() {
-  this.runCursorMovesOnDocument(this.simpleDoc, [
-    [CHARACTER, DIRECTIONAL, FORWARD, {index: 1, value: 'start '}],
+TEST_F(
+    'AccessibilityExtensionCursorsTest', 'CharacterWordCursor',
+    async function() {
+      await this.runCursorMovesOnDocument(this.simpleDoc, [
+        [CHARACTER, DIRECTIONAL, FORWARD, {index: 1, value: 'start '}],
 
-    [WORD, DIRECTIONAL, FORWARD, {index: 0, value: 'same line'}],
-    [CHARACTER, DIRECTIONAL, FORWARD, {index: 1, value: 'same line'}],
-    [WORD, DIRECTIONAL, FORWARD, {index: 5, value: 'same line'}],
-    [CHARACTER, DIRECTIONAL, BACKWARD, {index: 4, value: 'same line'}],
-    [WORD, DIRECTIONAL, FORWARD, {index: 5, value: 'same line'}],
-    [CHARACTER, DIRECTIONAL, FORWARD, {index: 6, value: 'same line'}],
-    [WORD, DIRECTIONAL, BACKWARD, {index: 0, value: 'same line'}],
-    [CHARACTER, DIRECTIONAL, BACKWARD, {index: 5, value: 'start '}],
-    [CHARACTER, DIRECTIONAL, BACKWARD, {index: 4, value: 'start '}],
-    [WORD, DIRECTIONAL, BACKWARD, {index: 0, value: 'start '}]
-  ]);
-});
+        [WORD, DIRECTIONAL, FORWARD, {index: 0, value: 'same line'}],
+        [CHARACTER, DIRECTIONAL, FORWARD, {index: 1, value: 'same line'}],
+        [WORD, DIRECTIONAL, FORWARD, {index: 5, value: 'same line'}],
+        [CHARACTER, DIRECTIONAL, BACKWARD, {index: 4, value: 'same line'}],
+        [WORD, DIRECTIONAL, FORWARD, {index: 5, value: 'same line'}],
+        [CHARACTER, DIRECTIONAL, FORWARD, {index: 6, value: 'same line'}],
+        [WORD, DIRECTIONAL, BACKWARD, {index: 0, value: 'same line'}],
+        [CHARACTER, DIRECTIONAL, BACKWARD, {index: 5, value: 'start '}],
+        [CHARACTER, DIRECTIONAL, BACKWARD, {index: 4, value: 'start '}],
+        [WORD, DIRECTIONAL, BACKWARD, {index: 0, value: 'start '}]
+      ]);
+    });
 
-TEST_F('AccessibilityExtensionCursorsTest', 'LineCursor', function() {
-  this.runCursorMovesOnDocument(this.simpleDoc, [
+TEST_F('AccessibilityExtensionCursorsTest', 'LineCursor', async function() {
+  await this.runCursorMovesOnDocument(this.simpleDoc, [
     // Line (BOUND).
     [LINE, BOUND, FORWARD, {value: 'same line'}],
     [LINE, BOUND, FORWARD, {value: 'same line'}],
@@ -226,8 +228,8 @@
   ]);
 });
 
-TEST_F('AccessibilityExtensionCursorsTest', 'SyncCursor', function() {
-  this.runCursorMovesOnDocument(this.simpleDoc, [
+TEST_F('AccessibilityExtensionCursorsTest', 'SyncCursor', async function() {
+  await this.runCursorMovesOnDocument(this.simpleDoc, [
     [WORD, SYNC, FORWARD, {index: 0, value: 'start '}],
 
     [NODE, DIRECTIONAL, FORWARD, {value: 'same line'}],
@@ -241,8 +243,8 @@
   ]);
 });
 
-TEST_F('AccessibilityExtensionCursorsTest', 'CharacterRange', function() {
-  this.runCursorMovesOnDocument(
+TEST_F('AccessibilityExtensionCursorsTest', 'CharacterRange', async function() {
+  await this.runCursorMovesOnDocument(
       this.simpleDoc,
       [
         [
@@ -303,8 +305,8 @@
       this.RANGE);
 });
 
-TEST_F('AccessibilityExtensionCursorsTest', 'WordRange', function() {
-  this.runCursorMovesOnDocument(
+TEST_F('AccessibilityExtensionCursorsTest', 'WordRange', async function() {
+  await this.runCursorMovesOnDocument(
       this.simpleDoc,
       [
         [
@@ -341,8 +343,8 @@
 });
 
 
-TEST_F('AccessibilityExtensionCursorsTest', 'LineRange', function() {
-  this.runCursorMovesOnDocument(
+TEST_F('AccessibilityExtensionCursorsTest', 'LineRange', async function() {
+  await this.runCursorMovesOnDocument(
       this.simpleDoc,
       [
         [LINE, FORWARD, {value: 'end', index: 0}, {value: 'end', index: 3}],
@@ -365,251 +367,238 @@
 
 TEST_F(
     'AccessibilityExtensionCursorsTest', 'DontSplitOnNodeNavigation',
-    function() {
-      this.runWithLoadedTree(this.multiInlineDoc, function(root) {
-        const para = root.firstChild;
-        assertEquals('paragraph', para.role);
-        let cursor = new cursors.Cursor(para.firstChild, 0);
-        cursor = cursor.move(NODE, DIRECTIONAL, FORWARD);
-        assertEquals('staticText', cursor.node.role);
-        assertEquals('end', cursor.node.name);
+    async function() {
+      const root = await this.runWithLoadedTree(this.multiInlineDoc);
+      const para = root.firstChild;
+      assertEquals('paragraph', para.role);
+      let cursor = new cursors.Cursor(para.firstChild, 0);
+      cursor = cursor.move(NODE, DIRECTIONAL, FORWARD);
+      assertEquals('staticText', cursor.node.role);
+      assertEquals('end', cursor.node.name);
 
-        cursor = cursor.move(NODE, DIRECTIONAL, BACKWARD);
-        assertEquals('staticText', cursor.node.role);
-        assertEquals('start diff line', cursor.node.name);
+      cursor = cursor.move(NODE, DIRECTIONAL, BACKWARD);
+      assertEquals('staticText', cursor.node.role);
+      assertEquals('start diff line', cursor.node.name);
 
-        assertEquals('inlineTextBox', cursor.node.firstChild.role);
-        assertEquals('start ', cursor.node.firstChild.name);
-        assertEquals('diff ', cursor.node.firstChild.nextSibling.name);
-        assertEquals('line', cursor.node.lastChild.name);
-      });
+      assertEquals('inlineTextBox', cursor.node.firstChild.role);
+      assertEquals('start ', cursor.node.firstChild.name);
+      assertEquals('diff ', cursor.node.firstChild.nextSibling.name);
+      assertEquals('line', cursor.node.lastChild.name);
     });
 
-TEST_F('AccessibilityExtensionCursorsTest', 'WrappingCursors', function() {
-  this.runWithLoadedTree(this.multiInlineDoc, function(root) {
-    const first = root;
-    const last = root.lastChild.firstChild;
-    let cursor = new cursors.WrappingCursor(first, -1);
+TEST_F(
+    'AccessibilityExtensionCursorsTest', 'WrappingCursors', async function() {
+      const root = await this.runWithLoadedTree(this.multiInlineDoc);
+      const first = root;
+      const last = root.lastChild.firstChild;
+      let cursor = new cursors.WrappingCursor(first, -1);
 
-    // Wrap from first node to last node.
-    cursor = cursor.move(NODE, DIRECTIONAL, BACKWARD);
-    assertEquals(last, cursor.node);
+      // Wrap from first node to last node.
+      cursor = cursor.move(NODE, DIRECTIONAL, BACKWARD);
+      assertEquals(last, cursor.node);
 
-    // Wrap from last node to first node.
-    cursor = cursor.move(NODE, DIRECTIONAL, FORWARD);
-    assertEquals(first, cursor.node);
-  });
-});
+      // Wrap from last node to first node.
+      cursor = cursor.move(NODE, DIRECTIONAL, FORWARD);
+      assertEquals(first, cursor.node);
+    });
 
-TEST_F('AccessibilityExtensionCursorsTest', 'IsInWebRange', function() {
-  this.runWithLoadedTree(this.simpleDoc, function(root) {
-    const para = root.firstChild;
-    const webRange = cursors.Range.fromNode(para);
-    const auraRange = cursors.Range.fromNode(root.parent);
-    assertFalse(auraRange.isWebRange());
-    assertTrue(webRange.isWebRange());
-  });
+TEST_F('AccessibilityExtensionCursorsTest', 'IsInWebRange', async function() {
+  const root = await this.runWithLoadedTree(this.simpleDoc);
+  const para = root.firstChild;
+  const webRange = cursors.Range.fromNode(para);
+  const auraRange = cursors.Range.fromNode(root.parent);
+  assertFalse(auraRange.isWebRange());
+  assertTrue(webRange.isWebRange());
 });
 
 // Disabled due to being flaky on ChromeOS. See https://crbug.com/1227435.
 TEST_F(
     'AccessibilityExtensionCursorsTest', 'DISABLED_SingleDocSelection',
-    function() {
-      this.runWithLoadedTree(
-          `
+    async function() {
+      const root = await this.runWithLoadedTree(`
     <span>start</span>
     <p><a href="google.com">google home page</a></p>
     <p>some more text</p>
     <p>end of text</p>
-  `,
-          function(root) {
-            // For some reason, Blink fails if we don't first select something
-            // on the page.
-            ChromeVoxState.instance.currentRange.select();
-            const link = root.find({role: RoleType.LINK});
-            const p1 = root.find({role: RoleType.PARAGRAPH});
-            const p2 = p1.nextSibling;
+  `);
+      // For some reason, Blink fails if we don't first select something
+      // on the page.
+      ChromeVoxState.instance.currentRange.select();
+      const link = root.find({role: RoleType.LINK});
+      const p1 = root.find({role: RoleType.PARAGRAPH});
+      const p2 = p1.nextSibling;
 
-            const singleSel = new cursors.Range(
-                new cursors.Cursor(link, 0), new cursors.Cursor(link, 1));
+      const singleSel = new cursors.Range(
+          new cursors.Cursor(link, 0), new cursors.Cursor(link, 1));
 
-            const multiSel = new cursors.Range(
-                new cursors.Cursor(p1.firstChild, 2),
-                new cursors.Cursor(p2.firstChild, 4));
+      const multiSel = new cursors.Range(
+          new cursors.Cursor(p1.firstChild, 2),
+          new cursors.Cursor(p2.firstChild, 4));
 
-            function verifySel() {
-              if (root.selectionStartObject === link.firstChild) {
-                assertEquals(link.firstChild, root.selectionStartObject);
-                assertEquals(0, root.selectionStartOffset);
-                assertEquals(link.firstChild, root.selectionEndObject);
-                assertEquals(1, root.selectionEndOffset);
-                this.listenOnce(root, 'textSelectionChanged', verifySel);
-                multiSel.select();
-              } else if (root.selectionStartObject === p1.firstChild) {
-                assertEquals(p1.firstChild, root.selectionStartObject);
-                assertEquals(2, root.selectionStartOffset);
-                assertEquals(p2.firstChild, root.selectionEndObject);
-                assertEquals(4, root.selectionEndOffset);
-              }
-            }
+      function verifySel() {
+        if (root.selectionStartObject === link.firstChild) {
+          assertEquals(link.firstChild, root.selectionStartObject);
+          assertEquals(0, root.selectionStartOffset);
+          assertEquals(link.firstChild, root.selectionEndObject);
+          assertEquals(1, root.selectionEndOffset);
+          this.listenOnce(root, 'textSelectionChanged', verifySel);
+          multiSel.select();
+        } else if (root.selectionStartObject === p1.firstChild) {
+          assertEquals(p1.firstChild, root.selectionStartObject);
+          assertEquals(2, root.selectionStartOffset);
+          assertEquals(p2.firstChild, root.selectionEndObject);
+          assertEquals(4, root.selectionEndOffset);
+        }
+      }
 
-            this.listenOnce(root, 'textSelectionChanged', verifySel, true);
-            singleSel.select();
-          });
+      this.listenOnce(root, 'textSelectionChanged', verifySel, true);
+      singleSel.select();
     });
 
-TEST_F('AccessibilityExtensionCursorsTest', 'InlineElementOffset', function() {
-  this.runWithLoadedTree(
-      `
+TEST_F(
+    'AccessibilityExtensionCursorsTest', 'InlineElementOffset',
+    async function() {
+      const root = await this.runWithLoadedTree(`
     <span>start</span>
     <p>This<br> is a<a href="#g">test</a>of selection</p>
-  `,
-      function(root) {
-        root.addEventListener(
-            'textSelectionChanged', this.newCallback(function(evt) {
-              // Test setup moves initial focus; ensure we don't test that here.
-              if (testNode !== root.selectionStartObject) {
-                return;
-              }
+  `);
+      root.addEventListener(
+          'textSelectionChanged', this.newCallback(function(evt) {
+            // Test setup moves initial focus; ensure we don't test that here.
+            if (testNode !== root.selectionStartObject) {
+              return;
+            }
 
-              // This is a little unexpected though not really incorrect; Ctrl+C
-              // works.
-              assertEquals(testNode, root.selectionStartObject);
-              assertEquals(ofSelectionNode, root.selectionEndObject);
-              assertEquals(4, root.selectionStartOffset);
-              assertEquals(1, root.selectionEndOffset);
-            }));
+            // This is a little unexpected though not really incorrect; Ctrl+C
+            // works.
+            assertEquals(testNode, root.selectionStartObject);
+            assertEquals(ofSelectionNode, root.selectionEndObject);
+            assertEquals(4, root.selectionStartOffset);
+            assertEquals(1, root.selectionEndOffset);
+          }));
 
-        // This is the link's static text.
-        const testNode = root.lastChild.lastChild.previousSibling.firstChild;
-        assertEquals(RoleType.STATIC_TEXT, testNode.role);
-        assertEquals('test', testNode.name);
+      // This is the link's static text.
+      const testNode = root.lastChild.lastChild.previousSibling.firstChild;
+      assertEquals(RoleType.STATIC_TEXT, testNode.role);
+      assertEquals('test', testNode.name);
 
-        const ofSelectionNode = root.lastChild.lastChild;
-        const cur = new cursors.Cursor(ofSelectionNode, 0);
-        assertEquals('of selection', cur.selectionNode.name);
-        assertEquals(RoleType.STATIC_TEXT, cur.selectionNode.role);
-        assertEquals(0, cur.selectionIndex);
+      const ofSelectionNode = root.lastChild.lastChild;
+      const cur = new cursors.Cursor(ofSelectionNode, 0);
+      assertEquals('of selection', cur.selectionNode.name);
+      assertEquals(RoleType.STATIC_TEXT, cur.selectionNode.role);
+      assertEquals(0, cur.selectionIndex);
 
-        const curIntoO = new cursors.Cursor(ofSelectionNode, 1);
-        assertEquals('of selection', curIntoO.selectionNode.name);
-        assertEquals(RoleType.STATIC_TEXT, curIntoO.selectionNode.role);
-        assertEquals(1, curIntoO.selectionIndex);
+      const curIntoO = new cursors.Cursor(ofSelectionNode, 1);
+      assertEquals('of selection', curIntoO.selectionNode.name);
+      assertEquals(RoleType.STATIC_TEXT, curIntoO.selectionNode.role);
+      assertEquals(1, curIntoO.selectionIndex);
 
-        const oRange = new cursors.Range(cur, curIntoO);
-        oRange.select();
-      });
-});
+      const oRange = new cursors.Range(cur, curIntoO);
+      oRange.select();
+    });
 
-TEST_F('AccessibilityExtensionCursorsTest', 'ContentEquality', function() {
-  this.runWithLoadedTree(
-      `
+TEST_F(
+    'AccessibilityExtensionCursorsTest', 'ContentEquality', async function() {
+      const root = await this.runWithLoadedTree(`
     <div role="region" aria-label="test region">this is a test</button>
-  `,
-      function(root) {
-        const region = root.firstChild;
-        assertEquals(RoleType.REGION, region.role);
-        const staticText = region.firstChild;
-        assertEquals(RoleType.STATIC_TEXT, staticText.role);
-        const inlineTextBox = staticText.firstChild;
-        assertEquals(RoleType.INLINE_TEXT_BOX, inlineTextBox.role);
+  `);
+      const region = root.firstChild;
+      assertEquals(RoleType.REGION, region.role);
+      const staticText = region.firstChild;
+      assertEquals(RoleType.STATIC_TEXT, staticText.role);
+      const inlineTextBox = staticText.firstChild;
+      assertEquals(RoleType.INLINE_TEXT_BOX, inlineTextBox.role);
 
-        const rootRange = cursors.Range.fromNode(root);
-        const regionRange = cursors.Range.fromNode(region);
-        const staticTextRange = cursors.Range.fromNode(staticText);
-        const inlineTextBoxRange = cursors.Range.fromNode(inlineTextBox);
+      const rootRange = cursors.Range.fromNode(root);
+      const regionRange = cursors.Range.fromNode(region);
+      const staticTextRange = cursors.Range.fromNode(staticText);
+      const inlineTextBoxRange = cursors.Range.fromNode(inlineTextBox);
 
-        // Positive cases.
-        assertTrue(regionRange.contentEquals(staticTextRange));
-        assertTrue(staticTextRange.contentEquals(regionRange));
-        assertTrue(inlineTextBoxRange.contentEquals(staticTextRange));
-        assertTrue(staticTextRange.contentEquals(inlineTextBoxRange));
+      // Positive cases.
+      assertTrue(regionRange.contentEquals(staticTextRange));
+      assertTrue(staticTextRange.contentEquals(regionRange));
+      assertTrue(inlineTextBoxRange.contentEquals(staticTextRange));
+      assertTrue(staticTextRange.contentEquals(inlineTextBoxRange));
 
-        // Negative cases.
-        assertFalse(rootRange.contentEquals(regionRange));
-        assertFalse(rootRange.contentEquals(staticTextRange));
-        assertFalse(rootRange.contentEquals(inlineTextBoxRange));
-        assertFalse(regionRange.contentEquals(rootRange));
-        assertFalse(staticTextRange.contentEquals(rootRange));
-        assertFalse(inlineTextBoxRange.contentEquals(rootRange));
-      });
-});
+      // Negative cases.
+      assertFalse(rootRange.contentEquals(regionRange));
+      assertFalse(rootRange.contentEquals(staticTextRange));
+      assertFalse(rootRange.contentEquals(inlineTextBoxRange));
+      assertFalse(regionRange.contentEquals(rootRange));
+      assertFalse(staticTextRange.contentEquals(rootRange));
+      assertFalse(inlineTextBoxRange.contentEquals(rootRange));
+    });
 
-TEST_F('AccessibilityExtensionCursorsTest', 'DeepEquivalency', function() {
-  this.runWithLoadedTree(
-      `
+TEST_F(
+    'AccessibilityExtensionCursorsTest', 'DeepEquivalency', async function() {
+      const root = await this.runWithLoadedTree(`
     <p style="word-spacing:100000px">this is a test</p>
-  `,
-      function(root) {
-        const textNode = root.find({role: RoleType.STATIC_TEXT});
+  `);
+      const textNode = root.find({role: RoleType.STATIC_TEXT});
 
-        let text = new cursors.Cursor(textNode, 2);
-        deep = text.deepEquivalent;
-        assertEquals('this ', deep.node.name);
-        assertEquals(RoleType.INLINE_TEXT_BOX, deep.node.role);
-        assertEquals(2, deep.index);
+      let text = new cursors.Cursor(textNode, 2);
+      deep = text.deepEquivalent;
+      assertEquals('this ', deep.node.name);
+      assertEquals(RoleType.INLINE_TEXT_BOX, deep.node.role);
+      assertEquals(2, deep.index);
 
-        text = new cursors.Cursor(textNode, 5);
-        deep = text.deepEquivalent;
-        assertEquals('is ', deep.node.name);
-        assertEquals(RoleType.INLINE_TEXT_BOX, deep.node.role);
-        assertEquals(0, deep.index);
+      text = new cursors.Cursor(textNode, 5);
+      deep = text.deepEquivalent;
+      assertEquals('is ', deep.node.name);
+      assertEquals(RoleType.INLINE_TEXT_BOX, deep.node.role);
+      assertEquals(0, deep.index);
 
-        text = new cursors.Cursor(textNode, 7);
-        deep = text.deepEquivalent;
-        assertEquals('is ', deep.node.name);
-        assertEquals(RoleType.INLINE_TEXT_BOX, deep.node.role);
-        assertEquals(2, deep.index);
+      text = new cursors.Cursor(textNode, 7);
+      deep = text.deepEquivalent;
+      assertEquals('is ', deep.node.name);
+      assertEquals(RoleType.INLINE_TEXT_BOX, deep.node.role);
+      assertEquals(2, deep.index);
 
-        text = new cursors.Cursor(textNode, 8);
-        deep = text.deepEquivalent;
-        assertEquals('a ', deep.node.name);
-        assertEquals(RoleType.INLINE_TEXT_BOX, deep.node.role);
-        assertEquals(0, deep.index);
+      text = new cursors.Cursor(textNode, 8);
+      deep = text.deepEquivalent;
+      assertEquals('a ', deep.node.name);
+      assertEquals(RoleType.INLINE_TEXT_BOX, deep.node.role);
+      assertEquals(0, deep.index);
 
-        text = new cursors.Cursor(textNode, 11);
-        deep = text.deepEquivalent;
-        assertEquals('test', deep.node.name);
-        assertEquals(RoleType.INLINE_TEXT_BOX, deep.node.role);
-        assertEquals(1, deep.index);
+      text = new cursors.Cursor(textNode, 11);
+      deep = text.deepEquivalent;
+      assertEquals('test', deep.node.name);
+      assertEquals(RoleType.INLINE_TEXT_BOX, deep.node.role);
+      assertEquals(1, deep.index);
 
-        // This is the only selection that can be placed at the length of the
-        // node's text. This only happens at the end of a line.
-        text = new cursors.Cursor(textNode, 14);
-        deep = text.deepEquivalent;
-        assertEquals('test', deep.node.name);
-        assertEquals(RoleType.INLINE_TEXT_BOX, deep.node.role);
-        assertEquals(4, deep.index);
+      // This is the only selection that can be placed at the length of the
+      // node's text. This only happens at the end of a line.
+      text = new cursors.Cursor(textNode, 14);
+      deep = text.deepEquivalent;
+      assertEquals('test', deep.node.name);
+      assertEquals(RoleType.INLINE_TEXT_BOX, deep.node.role);
+      assertEquals(4, deep.index);
 
-        // However, any offset larger is invalid.
-        text = new cursors.Cursor(textNode, 15);
-        deep = text.deepEquivalent;
-        assertTrue(text.equals(deep));
-      });
-});
+      // However, any offset larger is invalid.
+      text = new cursors.Cursor(textNode, 15);
+      deep = text.deepEquivalent;
+      assertTrue(text.equals(deep));
+    });
 
 TEST_F(
     'AccessibilityExtensionCursorsTest', 'DeepEquivalencyBeyondLastChild',
-    function() {
-      this.runWithLoadedTree(
-          `
+    async function() {
+      const root = await this.runWithLoadedTree(`
     <p>test</p>
-  `,
-          function(root) {
-            const paragraph = root.find({role: RoleType.PARAGRAPH});
-            assertEquals(1, paragraph.children.length);
-            const cursor = new cursors.Cursor(paragraph, 1);
+  `);
+      const paragraph = root.find({role: RoleType.PARAGRAPH});
+      assertEquals(1, paragraph.children.length);
+      const cursor = new cursors.Cursor(paragraph, 1);
 
-            const deep = cursor.deepEquivalent;
-            assertEquals(RoleType.STATIC_TEXT, deep.node.role);
-            assertEquals(4, deep.index);
-          });
+      const deep = cursor.deepEquivalent;
+      assertEquals(RoleType.STATIC_TEXT, deep.node.role);
+      assertEquals(4, deep.index);
     });
 
 TEST_F(
     'AccessibilityExtensionCursorsTest', 'MovementByWordThroughNonInlineText',
-    function() {
-      this.runCursorMovesOnDocument(this.buttonAndInlineTextDoc, [
+    async function() {
+      await this.runCursorMovesOnDocument(this.buttonAndInlineTextDoc, [
         // Move forward by word.
         // 'text' start and end indices.
         [WORD, DIRECTIONAL, FORWARD, {index: 7, value: 'Inline text content'}],
diff --git a/chrome/browser/resources/chromeos/accessibility/common/cursors/recovery_strategy_test.js b/chrome/browser/resources/chromeos/accessibility/common/cursors/recovery_strategy_test.js
index bf648a7..0c4f8ee 100644
--- a/chrome/browser/resources/chromeos/accessibility/common/cursors/recovery_strategy_test.js
+++ b/chrome/browser/resources/chromeos/accessibility/common/cursors/recovery_strategy_test.js
@@ -20,9 +20,8 @@
 
 TEST_F(
     'AccessibilityExtensionRecoveryStrategyTest', 'ReparentedRecovery',
-    function() {
-      this.runWithLoadedTree(
-          `
+    async function() {
+      const root = await this.runWithLoadedTree(`
     <input type="text"></input>
     <p id="p">hi</p>
     <button id="go"</button>
@@ -33,65 +32,63 @@
         document.body.appendChild(p);
       });
     </script>
-  `,
-          function(root) {
-            const p = root.find({role: RoleType.PARAGRAPH});
-            const s = root.find({role: RoleType.STATIC_TEXT});
-            const b = root.find({role: RoleType.BUTTON});
-            const bAncestryRecovery = new AncestryRecoveryStrategy(b);
-            const pAncestryRecovery = new AncestryRecoveryStrategy(p);
-            const sAncestryRecovery = new AncestryRecoveryStrategy(s);
-            const bTreePathRecovery = new TreePathRecoveryStrategy(b);
-            const pTreePathRecovery = new TreePathRecoveryStrategy(p);
-            const sTreePathRecovery = new TreePathRecoveryStrategy(s);
-            this.listenOnce(b, 'clicked', function() {
-              assertFalse(
-                  bAncestryRecovery.requiresRecovery(),
-                  'bAncestryRecovery.requiresRecovery');
-              assertTrue(
-                  pAncestryRecovery.requiresRecovery(),
-                  'pAncestryRecovery.requiresRecovery()');
-              assertTrue(
-                  sAncestryRecovery.requiresRecovery(),
-                  'sAncestryRecovery.requiresRecovery()');
-              assertFalse(
-                  bTreePathRecovery.requiresRecovery(),
-                  'bTreePathRecovery.requiresRecovery()');
-              assertTrue(
-                  pTreePathRecovery.requiresRecovery(),
-                  'pTreePathRecovery.requiresRecovery()');
-              assertTrue(
-                  sTreePathRecovery.requiresRecovery(),
-                  'sTreePathRecovery.requiresRecovery()');
+  `);
+      const p = root.find({role: RoleType.PARAGRAPH});
+      const s = root.find({role: RoleType.STATIC_TEXT});
+      const b = root.find({role: RoleType.BUTTON});
+      const bAncestryRecovery = new AncestryRecoveryStrategy(b);
+      const pAncestryRecovery = new AncestryRecoveryStrategy(p);
+      const sAncestryRecovery = new AncestryRecoveryStrategy(s);
+      const bTreePathRecovery = new TreePathRecoveryStrategy(b);
+      const pTreePathRecovery = new TreePathRecoveryStrategy(p);
+      const sTreePathRecovery = new TreePathRecoveryStrategy(s);
+      this.listenOnce(b, 'clicked', function() {
+        assertFalse(
+            bAncestryRecovery.requiresRecovery(),
+            'bAncestryRecovery.requiresRecovery');
+        assertTrue(
+            pAncestryRecovery.requiresRecovery(),
+            'pAncestryRecovery.requiresRecovery()');
+        assertTrue(
+            sAncestryRecovery.requiresRecovery(),
+            'sAncestryRecovery.requiresRecovery()');
+        assertFalse(
+            bTreePathRecovery.requiresRecovery(),
+            'bTreePathRecovery.requiresRecovery()');
+        assertTrue(
+            pTreePathRecovery.requiresRecovery(),
+            'pTreePathRecovery.requiresRecovery()');
+        assertTrue(
+            sTreePathRecovery.requiresRecovery(),
+            'sTreePathRecovery.requiresRecovery()');
 
-              assertEquals(RoleType.BUTTON, bAncestryRecovery.node.role);
-              assertEquals(root, pAncestryRecovery.node);
-              assertEquals(root, sAncestryRecovery.node);
+        assertEquals(RoleType.BUTTON, bAncestryRecovery.node.role);
+        assertEquals(root, pAncestryRecovery.node);
+        assertEquals(root, sAncestryRecovery.node);
 
-              assertEquals(b, bTreePathRecovery.node);
-              assertEquals(b, pTreePathRecovery.node);
-              assertEquals(b, sTreePathRecovery.node);
+        assertEquals(b, bTreePathRecovery.node);
+        assertEquals(b, pTreePathRecovery.node);
+        assertEquals(b, sTreePathRecovery.node);
 
-              assertFalse(
-                  bAncestryRecovery.requiresRecovery(),
-                  'bAncestryRecovery.requiresRecovery()');
-              assertFalse(
-                  pAncestryRecovery.requiresRecovery(),
-                  'pAncestryRecovery.requiresRecovery()');
-              assertFalse(
-                  sAncestryRecovery.requiresRecovery(),
-                  'sAncestryRecovery.requiresRecovery()');
-              assertFalse(
-                  bTreePathRecovery.requiresRecovery(),
-                  'bTreePathRecovery.requiresRecovery()');
-              assertFalse(
-                  pTreePathRecovery.requiresRecovery(),
-                  'pTreePathRecovery.requiresRecovery()');
-              assertFalse(
-                  sTreePathRecovery.requiresRecovery(),
-                  'sTreePathRecovery.requiresRecovery()');
-            });
-            // Trigger the change.
-            b.doDefault();
-          });
+        assertFalse(
+            bAncestryRecovery.requiresRecovery(),
+            'bAncestryRecovery.requiresRecovery()');
+        assertFalse(
+            pAncestryRecovery.requiresRecovery(),
+            'pAncestryRecovery.requiresRecovery()');
+        assertFalse(
+            sAncestryRecovery.requiresRecovery(),
+            'sAncestryRecovery.requiresRecovery()');
+        assertFalse(
+            bTreePathRecovery.requiresRecovery(),
+            'bTreePathRecovery.requiresRecovery()');
+        assertFalse(
+            pTreePathRecovery.requiresRecovery(),
+            'pTreePathRecovery.requiresRecovery()');
+        assertFalse(
+            sTreePathRecovery.requiresRecovery(),
+            'sTreePathRecovery.requiresRecovery()');
+      });
+      // Trigger the change.
+      b.doDefault();
     });
diff --git a/chrome/browser/resources/chromeos/accessibility/common/repeated_event_handler_test.js b/chrome/browser/resources/chromeos/accessibility/common/repeated_event_handler_test.js
index 83aa598..afa7e56 100644
--- a/chrome/browser/resources/chromeos/accessibility/common/repeated_event_handler_test.js
+++ b/chrome/browser/resources/chromeos/accessibility/common/repeated_event_handler_test.js
@@ -11,45 +11,43 @@
 /** Test fixture for array_util.js. */
 RepeatedEventHandlerTest = class extends ChromeVoxNextE2ETest {};
 
-TEST_F('RepeatedEventHandlerTest', 'RepeatedEventHandledOnce', function() {
-  this.runWithLoadedTree('', (root) => {
-    this.handlerCallCount = 0;
-    const handler = () => this.handlerCallCount++;
+TEST_F(
+    'RepeatedEventHandlerTest', 'RepeatedEventHandledOnce', async function() {
+      const root = await this.runWithLoadedTree('');
+      this.handlerCallCount = 0;
+      const handler = () => this.handlerCallCount++;
 
-    const repeatedHandler = new RepeatedEventHandler(root, 'focus', handler);
+      const repeatedHandler = new RepeatedEventHandler(root, 'focus', handler);
 
-    // Simulate events being fired.
-    repeatedHandler.onEvent_();
-    repeatedHandler.onEvent_();
-    repeatedHandler.onEvent_();
-    repeatedHandler.onEvent_();
-    repeatedHandler.onEvent_();
+      // Simulate events being fired.
+      repeatedHandler.onEvent_();
+      repeatedHandler.onEvent_();
+      repeatedHandler.onEvent_();
+      repeatedHandler.onEvent_();
+      repeatedHandler.onEvent_();
 
-    // Yield before verify how many times the handler was called.
-    setTimeout(
-        this.newCallback(() => assertEquals(this.handlerCallCount, 1)), 0);
-  });
-});
+      // Yield before verify how many times the handler was called.
+      setTimeout(
+          this.newCallback(() => assertEquals(this.handlerCallCount, 1)), 0);
+    });
 
 TEST_F(
     'RepeatedEventHandlerTest', 'NoEventsHandledAfterStopListening',
-    function() {
-      this.runWithLoadedTree('', (root) => {
-        this.handlerCallCount = 0;
-        const handler = () => this.handlerCallCount++;
+    async function() {
+      const root = await this.runWithLoadedTree('');
+      this.handlerCallCount = 0;
+      const handler = () => this.handlerCallCount++;
 
-        const repeatedHandler =
-            new RepeatedEventHandler(root, 'focus', handler);
+      const repeatedHandler = new RepeatedEventHandler(root, 'focus', handler);
 
-        // Simulate events being fired.
-        repeatedHandler.onEvent_();
-        repeatedHandler.onEvent_();
-        repeatedHandler.onEvent_();
+      // Simulate events being fired.
+      repeatedHandler.onEvent_();
+      repeatedHandler.onEvent_();
+      repeatedHandler.onEvent_();
 
-        repeatedHandler.stop();
+      repeatedHandler.stop();
 
-        // Yield before verifying how many times the handler was called.
-        setTimeout(
-            this.newCallback(() => assertEquals(this.handlerCallCount, 0)), 0);
-      });
+      // Yield before verifying how many times the handler was called.
+      setTimeout(
+          this.newCallback(() => assertEquals(this.handlerCallCount, 0)), 0);
     });
diff --git a/chrome/browser/resources/chromeos/accessibility/common/testing/e2e_test_base.js b/chrome/browser/resources/chromeos/accessibility/common/testing/e2e_test_base.js
index 831af49..90a52be2 100644
--- a/chrome/browser/resources/chromeos/accessibility/common/testing/e2e_test_base.js
+++ b/chrome/browser/resources/chromeos/accessibility/common/testing/e2e_test_base.js
@@ -135,6 +135,61 @@
   }
 
   /**
+   * Waits for the given |eventType| to be fired on |node|.
+   * @param {!chrome.automation.AutomationNode} node
+   * @param {!chrome.automation.EventType} eventType
+   */
+  async waitForEvent(node, eventType) {
+    return new Promise(resolve => {
+      const callback = () => {
+        node.removeEventListener(eventType, callback);
+        resolve();
+      };
+      node.addEventListener(eventType, callback);
+    });
+  }
+
+  /**
+   * @param {!chrome.automation.AutomationNode} app
+   * @return {boolean}
+   */
+  isInLacrosWindow(app) {
+    // We validate we're actually within a Lacros window by scanning upward
+    // until we see the presence of an app id, which indicates an app subtree.
+    // See go/lacros-accessibility for details.
+    while (app && !app.appId) {
+      app = app.parent;
+    }
+    return Boolean(app);
+  }
+
+  /**
+   * @param {string} url
+   * @param {!chrome.automation.AutomationNode} addressBar
+   */
+  async navigateToUrlForLacros(url, addressBar) {
+    // This populates the address bar as if we typed the url.
+    addressBar.setValue(url);
+
+    // We have two choices to confirm navigation.
+    if (!this.navigateLacrosWithAutoComplete) {
+      // 1. (default), hit enter.
+      await this.waitForEvent(addressBar, 'valueChanged');
+      EventGenerator.sendKeyPress(KeyCode.RETURN);
+    } else {
+      // 2. use the auto completion.
+      await this.waitForEvent(addressBar, 'controlsChanged');
+      // The text field relates to the auto complete list box via controlledBy.
+      // The |controls| node structure here nests several levels until the
+      // listBoxOption we want.
+      const autoCompleteListBoxOption =
+          addressBar.controls[0].firstChild.firstChild;
+      assertEquals('listBoxOption', autoCompleteListBoxOption.role);
+      autoCompleteListBoxOption.doDefault();
+    }
+  }
+
+  /**
    * Creates a callback that optionally calls {@code opt_callback} when
    * called.  If this method is called one or more times, then
    * {@code testDone()} will be called when all callbacks have been called.
@@ -150,7 +205,7 @@
   /**
    * Gets the desktop from the automation API and runs |callback|.
    * Arranges to call |testDone()| after |callback| returns.
-   * NOTE: Callbacks created inside |opt_callback| must be wrapped with
+   * NOTE: Callbacks created inside |callback| must be wrapped with
    * |this.newCallback| if passed to asynchronous calls.  Otherwise, the test
    * will be finished prematurely.
    * @param {function(chrome.automation.AutomationNode)} callback
@@ -162,128 +217,85 @@
 
   /**
    * Gets the desktop from the automation API and Launches a new tab with
-   * the given document, and runs |callback| when a load complete fires.
-   * Arranges to call |testDone()| after |callback| returns.
-   * NOTE: Callbacks created inside |callback| must be wrapped with
-   * |this.newCallback| if passed to asynchronous calls.  Otherwise, the test
-   * will be finished prematurely.
+   * the given document, and returns the root web area when a load complete
+   * fires.
    * @param {string|function(): string} doc An HTML snippet, optionally wrapped
    *     inside of a function.
-   * @param {function(chrome.automation.AutomationNode)} callback
-   *     Called with the root web area node once the document is ready.
-   * @param {{url: (string=), returnDesktop: (boolean=)}}
+   * @param {{url: (string=)}}
    *     opt_params
    *           url Optional url to wait for. Defaults to undefined.
+   * @return {chrome.automation.AutomationNode} the root web area node, only
+   *     returned once the document is ready.
    */
-  runWithLoadedTree(doc, callback, opt_params = {}) {
-    callback = this.newCallback(callback);
-    chrome.automation.getDesktop((desktop) => {
+  async runWithLoadedTree(doc, opt_params = {}) {
+    return new Promise(async resolve => {
+      // Make sure the test doesn't finish until this function has resolved.
+      let callback = this.newCallback(resolve);
+      this.desktop_ = await new Promise(r => chrome.automation.getDesktop(r));
       const url = opt_params.url || DocUtils.createUrlForDoc(doc);
 
-      chrome.commandLinePrivate.hasSwitch(
-          'lacros-chrome-path', hasLacrosChromePath => {
-            // The below block handles opening a url either in a Lacros tab or
-            // Ash tab. For Lacros, we re-use an already open Lacros tab. For
-            // Ash, we use the chrome.tabs api.
+      const hasLacrosChromePath = await new Promise(
+          r => chrome.commandLinePrivate.hasSwitch('lacros-chrome-path', r));
+      // The below block handles opening a url either in a Lacros tab or Ash
+      // tab. For Lacros, we re-use an already open Lacros tab. For Ash, we use
+      // the chrome.tabs api.
 
-            // This flag controls whether we've requested navigation to |url|
-            // within the open Lacros tab.
-            let didNavigateForLacros = false;
+      // This flag controls whether we've requested navigation to |url| within
+      // the open Lacros tab.
+      let didNavigateForLacros = false;
 
-            // Listens to both load completes and focus events to eventually
-            // trigger the test callback.
-            const listener = (event) => {
-              if (hasLacrosChromePath && !didNavigateForLacros) {
-                // We have yet to request navigation in the Lacros tab. Do so
-                // now by getting the default focus (the address bar), setting
-                // the value to the url and then performing do default on the
-                // auto completion node. This is somewhat involved, so each step
-                // is commented.
-                chrome.automation.getFocus(focus => {
-                  // It's possible focus is elsewhere; wait until it lands on
-                  // the address bar text field.
-                  if (focus.role !== chrome.automation.RoleType.TEXT_FIELD) {
-                    return;
-                  }
+      // Listener for both load complete and focus events that eventually
+      // triggers the test.
+      const listener = async (event) => {
+        if (hasLacrosChromePath && !didNavigateForLacros) {
+          // We have yet to request navigation in the Lacros tab. Do so now by
+          // getting the default focus (the address bar), setting the value to
+          // the url and then performing do default on the auto completion node.
+          const focus = await new Promise(r => chrome.automation.getFocus(r));
+          // It's possible focus is elsewhere; wait until it lands on the
+          // address bar text field.
+          if (focus.role !== chrome.automation.RoleType.TEXT_FIELD) {
+            return;
+          }
 
-                  // Next, we want to validate we're actually within a Lacros
-                  // window. Do so by scanning upward until we see the presence
-                  // of an app id which indicates an app subtree. See
-                  // go/lacros-accessibility for details.
-                  let app = focus;
-                  while (app && !app.appId) {
-                    app = app.parent;
-                  }
+          if (this.isInLacrosWindow(focus)) {
+            didNavigateForLacros = true;
+            await this.navigateToUrlForLacros(url, focus);
+          }
+          return;  // exit listener.
+        }
 
-                  if (app) {
-                    didNavigateForLacros = true;
+        // Navigation has occurred, but we need to ensure the url we want has
+        // loaded.
+        if (event.target.root.url !== url || !event.target.root.docLoaded) {
+          return;  // exit listener.
+        }
 
-                    // This populates the address bar as if we typed the url.
-                    focus.setValue(url);
+        // Finally, when we get here, we've successfully navigated to
+        // the |url| in either Lacros or Ash.
+        this.desktop_.removeEventListener('focus', listener, true);
+        this.desktop_.removeEventListener('loadComplete', listener, true);
 
-                    // We have two choices to confirm navigation.
-                    if (!this.navigateLacrosWithAutoComplete) {
-                      // 1. (default), hit enter.
-                      const onValueChanged = e => {
-                        focus.removeEventListener(
-                            'valueChanged', onValueChanged);
-                        EventGenerator.sendKeyPress(KeyCode.RETURN);
-                      };
-                      focus.addEventListener('valueChanged', onValueChanged);
-                    } else {
-                      // 2. use the auto completion.
-                      // Wait until the auto completion shows up.
-                      const clickAutocomplete = () => {
-                        focus.removeEventListener(
-                            'controlsChanged', clickAutocomplete);
+        if (callback) {
+          callback(event.target.root);
+        }
+        // Avoid calling |callback| twice (which would cause the test to fail).
+        callback = null;
+      };  // end listener.
 
-                        // The text field relates to the auto complete list box
-                        // via controlledBy. The |controls| node structure here
-                        // nests several levels until the listBoxOption we want.
-                        const autoCompleteListBoxOption =
-                            focus.controls[0].firstChild.firstChild;
-                        assertEquals(
-                            'listBoxOption', autoCompleteListBoxOption.role);
-                        autoCompleteListBoxOption.doDefault();
-                      };
-                      focus.addEventListener(
-                          'controlsChanged', clickAutocomplete);
-                    }
-                  }
-                });
-                return;
-              }
+      // Setup the listener above for focus and load complete listening.
+      this.desktop_.addEventListener('focus', listener, true);
+      this.desktop_.addEventListener('loadComplete', listener, true);
 
-              // Navigation has occurred, but we need to ensure the url we want
-              // has loaded.
-              if (event.target.root.url !== url ||
-                  !event.target.root.docLoaded) {
-                return;
-              }
-
-              // Finally, when we get here, we've successfully navigated to the
-              // |url| in either Lacros or Ash.
-              desktop.removeEventListener('focus', listener, true);
-              desktop.removeEventListener('loadComplete', listener, true);
-              callback && callback(event.target.root);
-              callback = null;
-            };
-
-            // Setup the listener above for focus and load complete listening.
-            this.desktop_ = desktop;
-            desktop.addEventListener('focus', listener, true);
-            desktop.addEventListener('loadComplete', listener, true);
-
-            // The easy case -- just open the Ash tab.
-            if (!hasLacrosChromePath) {
-              const createParams = {active: true, url};
-              chrome.tabs.create(createParams);
-            } else {
-              chrome.automation.getFocus(f => {
-                listener({target: f});
-              });
-            }
-          });
+      // The easy case -- just open the Ash tab.
+      if (!hasLacrosChromePath) {
+        const createParams = {active: true, url};
+        chrome.tabs.create(createParams);
+      } else {
+        chrome.automation.getFocus(f => {
+          listener({target: f});
+        });
+      }
     });
   }
 
diff --git a/chrome/browser/resources/chromeos/accessibility/common/tree_walker_test.js b/chrome/browser/resources/chromeos/accessibility/common/tree_walker_test.js
index f6c890a8..992a476a 100644
--- a/chrome/browser/resources/chromeos/accessibility/common/tree_walker_test.js
+++ b/chrome/browser/resources/chromeos/accessibility/common/tree_walker_test.js
@@ -121,9 +121,8 @@
 
 TEST_F(
     'AccessibilityExtensionAutomationTreeWalkerTest', 'RootLeafRestriction',
-    function() {
-      this.runWithLoadedTree(
-          `
+    async function() {
+      const r = await this.runWithLoadedTree(`
       <div role="group" aria-label="1">
         <div role="group" aria-label="2">
           <div role="group" aria-label="3">
@@ -133,121 +132,116 @@
         </div>
         <div role="group" aria-label="6"></div>
       </div>
-    `,
-          function(r) {
-            const node2 = r.firstChild.firstChild;
-            assertEquals('2', node2.name);
+    `);
+      const node2 = r.firstChild.firstChild;
+      assertEquals('2', node2.name);
 
-            // Restrict to 2's subtree and consider 3 and 5 leaves.
-            const leafP = function(n) {
-              return n.name === '3' || n.name === '5';
-            };
-            const rootP = function(n) {
-              return n.name === '2';
-            };
+      // Restrict to 2's subtree and consider 3 and 5 leaves.
+      const leafP = function(n) {
+        return n.name === '3' || n.name === '5';
+      };
+      const rootP = function(n) {
+        return n.name === '2';
+      };
 
-            // Track the nodes we've visited.
-            let visited = '';
-            const visit = function(n) {
-              visited += n.name;
-            };
-            const restrictions = {leaf: leafP, root: rootP, visit};
-            let walker =
-                new AutomationTreeWalker(node2, 'forward', restrictions);
-            while (walker.next().node) {
-            }
-            assertEquals('35', visited);
-            assertEquals(AutomationTreeWalkerPhase.OTHER, walker.phase);
+      // Track the nodes we've visited.
+      let visited = '';
+      const visit = function(n) {
+        visited += n.name;
+      };
+      const restrictions = {leaf: leafP, root: rootP, visit};
+      let walker = new AutomationTreeWalker(node2, 'forward', restrictions);
+      while (walker.next().node) {
+      }
+      assertEquals('35', visited);
+      assertEquals(AutomationTreeWalkerPhase.OTHER, walker.phase);
 
-            // And the reverse.
-            // Note that walking into a root is allowed.
-            visited = '';
-            const node6 = r.lastChild.lastChild;
-            assertEquals('6', node6.name);
-            walker = new AutomationTreeWalker(node6, 'backward', restrictions);
-            while (walker.next().node) {
-            }
-            assertEquals('532', visited);
+      // And the reverse.
+      // Note that walking into a root is allowed.
+      visited = '';
+      const node6 = r.lastChild.lastChild;
+      assertEquals('6', node6.name);
+      walker = new AutomationTreeWalker(node6, 'backward', restrictions);
+      while (walker.next().node) {
+      }
+      assertEquals('532', visited);
 
-            // Test not visiting ancestors of initial node.
-            const node5 = r.firstChild.firstChild.lastChild;
-            assertEquals('5', node5.name);
-            restrictions.root = function(n) {
-              return n.name === '1';
-            };
-            restrictions.leaf = function(n) {
-              return !n.firstChild;
-            };
+      // Test not visiting ancestors of initial node.
+      const node5 = r.firstChild.firstChild.lastChild;
+      assertEquals('5', node5.name);
+      restrictions.root = function(n) {
+        return n.name === '1';
+      };
+      restrictions.leaf = function(n) {
+        return !n.firstChild;
+      };
 
-            visited = '';
-            restrictions.skipInitialAncestry = false;
-            walker = new AutomationTreeWalker(node5, 'backward', restrictions);
-            while (walker.next().node) {
-            }
-            assertEquals('4321', visited);
+      visited = '';
+      restrictions.skipInitialAncestry = false;
+      walker = new AutomationTreeWalker(node5, 'backward', restrictions);
+      while (walker.next().node) {
+      }
+      assertEquals('4321', visited);
 
-            // 2 and 1 are ancestors; check they get skipped.
-            visited = '';
-            restrictions.skipInitialAncestry = true;
-            walker = new AutomationTreeWalker(node5, 'backward', restrictions);
-            while (walker.next().node) {
-            }
-            assertEquals('43', visited);
+      // 2 and 1 are ancestors; check they get skipped.
+      visited = '';
+      restrictions.skipInitialAncestry = true;
+      walker = new AutomationTreeWalker(node5, 'backward', restrictions);
+      while (walker.next().node) {
+      }
+      assertEquals('43', visited);
 
-            // We should skip node 2's subtree.
-            walker = new AutomationTreeWalker(
-                node2, 'forward', {skipInitialSubtree: true});
-            assertEquals(node6, walker.next().node);
-          });
+      // We should skip node 2's subtree.
+      walker = new AutomationTreeWalker(
+          node2, 'forward', {skipInitialSubtree: true});
+      assertEquals(node6, walker.next().node);
     });
 
 TEST_F(
     'AccessibilityExtensionAutomationTreeWalkerTest', 'LeafPredicateSymmetry',
-    function() {
-      this.runWithLoadedTree(toolbarDoc, function(r) {
-        const d = r.root.parent.root;
-        const forwardWalker = new AutomationTreeWalker(d, 'forward');
-        const forwardNodes = [];
+    async function() {
+      const r = await this.runWithLoadedTree(toolbarDoc);
+      const d = r.root.parent.root;
+      const forwardWalker = new AutomationTreeWalker(d, 'forward');
+      const forwardNodes = [];
 
-        // Get all nodes according to the walker in the forward direction.
-        do {
-          forwardNodes.push(forwardWalker.node);
-        } while (forwardWalker.next().node);
+      // Get all nodes according to the walker in the forward direction.
+      do {
+        forwardNodes.push(forwardWalker.node);
+      } while (forwardWalker.next().node);
 
-        // Now, verify the walker moving backwards matches the forwards list.
-        const backwardWalker = new AutomationTreeWalker(
-            forwardNodes[forwardNodes.length - 1], 'backward');
+      // Now, verify the walker moving backwards matches the forwards list.
+      const backwardWalker = new AutomationTreeWalker(
+          forwardNodes[forwardNodes.length - 1], 'backward');
 
-        do {
-          const next = forwardNodes.pop();
-          assertEquals(next, backwardWalker.node);
-        } while (backwardWalker.next().node);
-      });
+      do {
+        const next = forwardNodes.pop();
+        assertEquals(next, backwardWalker.node);
+      } while (backwardWalker.next().node);
     });
 
 TEST_F(
     'AccessibilityExtensionAutomationTreeWalkerTest', 'RootPredicateEnding',
-    function() {
-      this.runWithLoadedTree(toolbarDoc(), function(r) {
-        const backwardWalker =
-            new AutomationTreeWalker(r.firstChild, 'backward', {
-              root(node) {
-                return node === r;
-              }
-            });
-        assertEquals(r, backwardWalker.next().node);
-        assertEquals(null, backwardWalker.next().node);
+    async function() {
+      const r = await this.runWithLoadedTree(toolbarDoc());
+      const backwardWalker =
+          new AutomationTreeWalker(r.firstChild, 'backward', {
+            root(node) {
+              return node === r;
+            }
+          });
+      assertEquals(r, backwardWalker.next().node);
+      assertEquals(null, backwardWalker.next().node);
 
-        const forwardWalker =
-            new AutomationTreeWalker(r.firstChild.lastChild, 'forward', {
-              root(node) {
-                return node === r;
-              }
-            });
-        // Advance to the static text box of button contains text "Forward".
-        assertEquals('Forward', forwardWalker.next().node.name);
-        // Advance to the inline text box of button contains text "Forward".
-        assertEquals('Forward', forwardWalker.next().node.name);
-        assertEquals(null, forwardWalker.next().node);
-      });
+      const forwardWalker =
+          new AutomationTreeWalker(r.firstChild.lastChild, 'forward', {
+            root(node) {
+              return node === r;
+            }
+          });
+      // Advance to the static text box of button contains text "Forward".
+      assertEquals('Forward', forwardWalker.next().node.name);
+      // Advance to the inline text box of button contains text "Forward".
+      assertEquals('Forward', forwardWalker.next().node.name);
+      assertEquals(null, forwardWalker.next().node);
     });
diff --git a/chrome/browser/resources/chromeos/accessibility/select_to_speak/paragraph_utils_overflow_test.js b/chrome/browser/resources/chromeos/accessibility/select_to_speak/paragraph_utils_overflow_test.js
index 5ba8abb..4c09c27de 100644
--- a/chrome/browser/resources/chromeos/accessibility/select_to_speak/paragraph_utils_overflow_test.js
+++ b/chrome/browser/resources/chromeos/accessibility/select_to_speak/paragraph_utils_overflow_test.js
@@ -43,126 +43,123 @@
 
 TEST_F(
     'SelectToSpeakParagraphOverflowTest',
-    'ReplaceseHorizentalOverflowTextWithSpace', function() {
+    'ReplaceseHorizentalOverflowTextWithSpace', async function() {
       const inputText = 'This text overflows partially';
-      this.runWithLoadedTree(
-          this.generateHorizentalOverflowText(inputText), function(root) {
-            const overflowText = root.find({
-              role: chrome.automation.RoleType.INLINE_TEXT_BOX,
-              attributes: {name: inputText},
-            });
-            var nodeGroup = ParagraphUtils.buildNodeGroup(
-                [overflowText], 0 /* index */, {clipOverflowWords: true});
+      const root = await this.runWithLoadedTree(
+          this.generateHorizentalOverflowText(inputText));
+      const overflowText = root.find({
+        role: chrome.automation.RoleType.INLINE_TEXT_BOX,
+        attributes: {name: inputText},
+      });
+      var nodeGroup = ParagraphUtils.buildNodeGroup(
+          [overflowText], 0 /* index */, {clipOverflowWords: true});
 
-            // The output text should have the same length of the input text
-            // plus a space character at the end.
-            assertEquals(nodeGroup.text.length, inputText.length + 1);
-            // The output text should have less non-empty characters compared
-            // to the input text, as any overflow word will be replaced as
-            // space characters.
-            assertTrue(
-                nodeGroup.text.replace(/ /g, '').length <
-                inputText.replace(/ /g, '').length);
-          });
+      // The output text should have the same length of the input text
+      // plus a space character at the end.
+      assertEquals(nodeGroup.text.length, inputText.length + 1);
+      // The output text should have less non-empty characters compared
+      // to the input text, as any overflow word will be replaced as
+      // space characters.
+      assertTrue(
+          nodeGroup.text.replace(/ /g, '').length <
+          inputText.replace(/ /g, '').length);
     });
 
 TEST_F(
     'SelectToSpeakParagraphOverflowTest',
-    'ReplaceseVerticalOverflowTextWithSpace', function() {
+    'ReplaceseVerticalOverflowTextWithSpace', async function() {
       const visibleText = 'This text is visible';
       const overflowText = 'This text overflows';
-      this.runWithLoadedTree(
-          this.generateVerticalOverflowText(visibleText, overflowText),
-          function(root) {
-            // Find the visible text.
-            const visibleTextNode = root.find({
-              role: chrome.automation.RoleType.INLINE_TEXT_BOX,
-              attributes: {name: visibleText},
-            });
-            var nodeGroup = ParagraphUtils.buildNodeGroup(
-                [visibleTextNode], 0 /* index */, {clipOverflowWords: true});
-            // The output text should have the same length of the visible text
-            // plus a space character at the end.
-            assertEquals(nodeGroup.text.length, visibleText.length + 1);
-            // The output text should be the same of the input text.
-            assertEquals(
-                nodeGroup.text.replace(/ /g, ''),
-                visibleText.replace(/ /g, ''));
+      const root = await this.runWithLoadedTree(
+          this.generateVerticalOverflowText(visibleText, overflowText));
+      // Find the visible text.
+      const visibleTextNode = root.find({
+        role: chrome.automation.RoleType.INLINE_TEXT_BOX,
+        attributes: {name: visibleText},
+      });
+      var nodeGroup = ParagraphUtils.buildNodeGroup(
+          [visibleTextNode], 0 /* index */, {clipOverflowWords: true});
+      // The output text should have the same length of the visible text
+      // plus a space character at the end.
+      assertEquals(nodeGroup.text.length, visibleText.length + 1);
+      // The output text should be the same of the input text.
+      assertEquals(
+          nodeGroup.text.replace(/ /g, ''), visibleText.replace(/ /g, ''));
 
-            // Find the overflow text.
-            const overflowTextNode = root.find({
-              role: chrome.automation.RoleType.INLINE_TEXT_BOX,
-              attributes: {name: overflowText},
-            });
-            var nodeGroup = ParagraphUtils.buildNodeGroup(
-                [overflowTextNode], 0 /* index */, {clipOverflowWords: true});
+      // Find the overflow text.
+      const overflowTextNode = root.find({
+        role: chrome.automation.RoleType.INLINE_TEXT_BOX,
+        attributes: {name: overflowText},
+      });
+      var nodeGroup = ParagraphUtils.buildNodeGroup(
+          [overflowTextNode], 0 /* index */, {clipOverflowWords: true});
 
-            // The output text should have the same length of the overflow text
-            // plus a space character at the end.
-            assertEquals(nodeGroup.text.length, overflowText.length + 1);
-            // The output text should only have space characters.
-            assertEquals(nodeGroup.text.replace(/ /g, '').length, 0);
-          });
+      // The output text should have the same length of the overflow text
+      // plus a space character at the end.
+      assertEquals(nodeGroup.text.length, overflowText.length + 1);
+      // The output text should only have space characters.
+      assertEquals(nodeGroup.text.replace(/ /g, '').length, 0);
     });
 
 TEST_F(
     'SelectToSpeakParagraphOverflowTest',
-    'ReplacesEntirelyOverflowTextWithSpace', function() {
+    'ReplacesEntirelyOverflowTextWithSpace', async function() {
       const inputText = 'This text overflows entirely';
-      this.runWithLoadedTree(
-          this.generateEntirelyOverflowText(inputText), function(root) {
-            const overflowText = root.find({
-              role: chrome.automation.RoleType.INLINE_TEXT_BOX,
-              attributes: {name: inputText},
-            });
-            var nodeGroup = ParagraphUtils.buildNodeGroup(
-                [overflowText], 0 /* index */, {clipOverflowWords: true});
+      const root = await this.runWithLoadedTree(
+          this.generateEntirelyOverflowText(inputText));
+      const overflowText = root.find({
+        role: chrome.automation.RoleType.INLINE_TEXT_BOX,
+        attributes: {name: inputText},
+      });
+      var nodeGroup = ParagraphUtils.buildNodeGroup(
+          [overflowText], 0 /* index */, {clipOverflowWords: true});
 
-            // The output text should have the same length of the input text
-            // plus a space character at the end.
-            assertEquals(nodeGroup.text.length, inputText.length + 1);
-            // The output text should have zero non-empty character.
-            assertEquals(nodeGroup.text.replace(/ /g, '').length, 0);
-          });
+      // The output text should have the same length of the input text
+      // plus a space character at the end.
+      assertEquals(nodeGroup.text.length, inputText.length + 1);
+      // The output text should have zero non-empty character.
+      assertEquals(nodeGroup.text.replace(/ /g, '').length, 0);
     });
 
-TEST_F('SelectToSpeakParagraphOverflowTest', 'OutputsVisibleText', function() {
-  const inputText = 'This text is visible';
-  this.runWithLoadedTree(this.generateVisibleText(inputText), function(root) {
-    const visibleText = root.find({
-      role: chrome.automation.RoleType.INLINE_TEXT_BOX,
-      attributes: {name: inputText},
-    });
-    var nodeGroup = ParagraphUtils.buildNodeGroup(
-        [visibleText], 0 /* index */, {clipOverflowWords: true});
+TEST_F(
+    'SelectToSpeakParagraphOverflowTest', 'OutputsVisibleText',
+    async function() {
+      const inputText = 'This text is visible';
+      const root =
+          await this.runWithLoadedTree(this.generateVisibleText(inputText));
+      const visibleText = root.find({
+        role: chrome.automation.RoleType.INLINE_TEXT_BOX,
+        attributes: {name: inputText},
+      });
+      var nodeGroup = ParagraphUtils.buildNodeGroup(
+          [visibleText], 0 /* index */, {clipOverflowWords: true});
 
-    // The output text should have the same length of the input text plus a
-    // space character at the end.
-    assertEquals(nodeGroup.text.length, inputText.length + 1);
-    // The output text should have same non-empty words as the input text.
-    assertEquals(nodeGroup.text.replace(/ /g, ''), inputText.replace(/ /g, ''));
-  });
-});
+      // The output text should have the same length of the input text plus a
+      // space character at the end.
+      assertEquals(nodeGroup.text.length, inputText.length + 1);
+      // The output text should have same non-empty words as the input text.
+      assertEquals(
+          nodeGroup.text.replace(/ /g, ''), inputText.replace(/ /g, ''));
+    });
 
 TEST_F(
     'SelectToSpeakParagraphOverflowTest',
-    'DoesNotClipOverflowWordsWhenDisabled', function() {
+    'DoesNotClipOverflowWordsWhenDisabled', async function() {
       const inputText = 'This text overflows entirely';
-      this.runWithLoadedTree(
-          this.generateEntirelyOverflowText(inputText), function(root) {
-            const overflowText = root.find({
-              role: chrome.automation.RoleType.INLINE_TEXT_BOX,
-              attributes: {name: inputText},
-            });
-            var nodeGroup = ParagraphUtils.buildNodeGroup(
-                [overflowText], 0 /* index */, {clipOverflowWords: false});
+      const root = await this.runWithLoadedTree(
+          this.generateEntirelyOverflowText(inputText));
+      const overflowText = root.find({
+        role: chrome.automation.RoleType.INLINE_TEXT_BOX,
+        attributes: {name: inputText},
+      });
+      var nodeGroup = ParagraphUtils.buildNodeGroup(
+          [overflowText], 0 /* index */, {clipOverflowWords: false});
 
-            // The output text should have the same length of the input text
-            // plus a space character at the end.
-            assertEquals(nodeGroup.text.length, inputText.length + 1);
-            // The output text should have same non-empty words as the input
-            // text.
-            assertEquals(
-                nodeGroup.text.replace(/ /g, ''), inputText.replace(/ /g, ''));
-          });
+      // The output text should have the same length of the input text
+      // plus a space character at the end.
+      assertEquals(nodeGroup.text.length, inputText.length + 1);
+      // The output text should have same non-empty words as the input
+      // text.
+      assertEquals(
+          nodeGroup.text.replace(/ /g, ''), inputText.replace(/ /g, ''));
     });
diff --git a/chrome/browser/resources/chromeos/accessibility/select_to_speak/select_to_speak_enhanced_voices_test.js b/chrome/browser/resources/chromeos/accessibility/select_to_speak/select_to_speak_enhanced_voices_test.js
index ae41a7e5..3ac8409 100644
--- a/chrome/browser/resources/chromeos/accessibility/select_to_speak/select_to_speak_enhanced_voices_test.js
+++ b/chrome/browser/resources/chromeos/accessibility/select_to_speak/select_to_speak_enhanced_voices_test.js
@@ -66,133 +66,112 @@
 
 TEST_F(
     'SelectToSpeakEnhancedNetworkTtsVoicesTest',
-    'EnablesVoicesIfConfirmedInDialog', function() {
+    'EnablesVoicesIfConfirmedInDialog', async function() {
       this.confirmationDialogResponse_ = true;
 
-      this.runWithLoadedTree(
+      const root = await this.runWithLoadedTree(
           'data:text/html;charset=utf-8,' +
-              '<p>This is some text</p>',
-          function(root) {
-            assertFalse(this.mockTts.currentlySpeaking());
-            assertEquals(this.mockTts.pendingUtterances().length, 0);
-            this.mockTts.setOnSpeechCallbacks([this.newCallback(function(
-                utterance) {
-              // Speech starts asynchronously.
-              assertEquals(this.confirmationDialogShowCount_, 1);
-              assertTrue(
-                  selectToSpeak.prefsManager_.enhancedVoicesDialogShown());
-              assertTrue(
-                  selectToSpeak.prefsManager_.enhancedNetworkVoicesEnabled());
-              assertTrue(this.mockTts.currentlySpeaking());
-              assertEquals(this.mockTts.pendingUtterances().length, 1);
-              this.assertEqualsCollapseWhitespace(
-                  this.mockTts.pendingUtterances()[0], 'This is some text');
-            })]);
-            const textNode = this.findTextNode(root, 'This is some text');
-            const event = {
-              screenX: textNode.location.left + 1,
-              screenY: textNode.location.top + 1
-            };
-            this.triggerReadMouseSelectedText(event, event);
-          });
+          '<p>This is some text</p>');
+      assertFalse(this.mockTts.currentlySpeaking());
+      assertEquals(this.mockTts.pendingUtterances().length, 0);
+      this.mockTts.setOnSpeechCallbacks([this.newCallback(function(utterance) {
+        // Speech starts asynchronously.
+        assertEquals(this.confirmationDialogShowCount_, 1);
+        assertTrue(selectToSpeak.prefsManager_.enhancedVoicesDialogShown());
+        assertTrue(selectToSpeak.prefsManager_.enhancedNetworkVoicesEnabled());
+        assertTrue(this.mockTts.currentlySpeaking());
+        assertEquals(this.mockTts.pendingUtterances().length, 1);
+        this.assertEqualsCollapseWhitespace(
+            this.mockTts.pendingUtterances()[0], 'This is some text');
+      })]);
+      const textNode = this.findTextNode(root, 'This is some text');
+      const event = {
+        screenX: textNode.location.left + 1,
+        screenY: textNode.location.top + 1
+      };
+      this.triggerReadMouseSelectedText(event, event);
     });
 
 TEST_F(
     'SelectToSpeakEnhancedNetworkTtsVoicesTest',
-    'DisablesVoicesIfCanceledInDialog', function() {
+    'DisablesVoicesIfCanceledInDialog', async function() {
       this.confirmationDialogResponse_ = false;
-      this.runWithLoadedTree(
+      const root = await this.runWithLoadedTree(
           'data:text/html;charset=utf-8,' +
-              '<p>This is some text</p>',
-          function(root) {
-            assertFalse(this.mockTts.currentlySpeaking());
-            assertEquals(this.mockTts.pendingUtterances().length, 0);
-            this.mockTts.setOnSpeechCallbacks([this.newCallback(function(
-                utterance) {
-              // Speech starts asynchronously.
-              assertEquals(this.confirmationDialogShowCount_, 1);
-              assertTrue(
-                  selectToSpeak.prefsManager_.enhancedVoicesDialogShown());
-              assertFalse(
-                  selectToSpeak.prefsManager_.enhancedNetworkVoicesEnabled());
-              assertTrue(this.mockTts.currentlySpeaking());
-              assertEquals(this.mockTts.pendingUtterances().length, 1);
-              this.assertEqualsCollapseWhitespace(
-                  this.mockTts.pendingUtterances()[0], 'This is some text');
-            })]);
-            const textNode = this.findTextNode(root, 'This is some text');
-            const event = {
-              screenX: textNode.location.left + 1,
-              screenY: textNode.location.top + 1
-            };
-            this.triggerReadMouseSelectedText(event, event);
-          });
+          '<p>This is some text</p>');
+      assertFalse(this.mockTts.currentlySpeaking());
+      assertEquals(this.mockTts.pendingUtterances().length, 0);
+      this.mockTts.setOnSpeechCallbacks([this.newCallback(function(utterance) {
+        // Speech starts asynchronously.
+        assertEquals(this.confirmationDialogShowCount_, 1);
+        assertTrue(selectToSpeak.prefsManager_.enhancedVoicesDialogShown());
+        assertFalse(selectToSpeak.prefsManager_.enhancedNetworkVoicesEnabled());
+        assertTrue(this.mockTts.currentlySpeaking());
+        assertEquals(this.mockTts.pendingUtterances().length, 1);
+        this.assertEqualsCollapseWhitespace(
+            this.mockTts.pendingUtterances()[0], 'This is some text');
+      })]);
+      const textNode = this.findTextNode(root, 'This is some text');
+      const event = {
+        screenX: textNode.location.left + 1,
+        screenY: textNode.location.top + 1
+      };
+      this.triggerReadMouseSelectedText(event, event);
     });
 
 TEST_F(
     'SelectToSpeakEnhancedNetworkTtsVoicesTest',
-    'DisablesVoicesIfDisallowedByPolicy', function() {
+    'DisablesVoicesIfDisallowedByPolicy', async function() {
       this.confirmationDialogResponse_ = true;
 
-      this.runWithLoadedTree(
+      const root = await this.runWithLoadedTree(
           'data:text/html;charset=utf-8,' +
-              '<p>This is some text</p>',
-          function(root) {
-            this.mockTts.setOnSpeechCallbacks([this.newCallback(function(
-                utterance) {
-              // Network voices are enabled initially because of the
-              // confirmation.
-              assertEquals(this.confirmationDialogShowCount_, 1);
-              assertTrue(
-                  selectToSpeak.prefsManager_.enhancedVoicesDialogShown());
-              assertTrue(
-                  selectToSpeak.prefsManager_.enhancedNetworkVoicesEnabled());
+          '<p>This is some text</p>');
+      this.mockTts.setOnSpeechCallbacks([this.newCallback(function(utterance) {
+        // Network voices are enabled initially because of the
+        // confirmation.
+        assertEquals(this.confirmationDialogShowCount_, 1);
+        assertTrue(selectToSpeak.prefsManager_.enhancedVoicesDialogShown());
+        assertTrue(selectToSpeak.prefsManager_.enhancedNetworkVoicesEnabled());
 
-              // Sets the policy to disallow network voices.
-              this.setEnhancedNetworkVoicesPolicy(/* allowed= */ false);
-              assertFalse(
-                  selectToSpeak.prefsManager_.enhancedNetworkVoicesEnabled());
-            })]);
-            const textNode = this.findTextNode(root, 'This is some text');
-            const event = {
-              screenX: textNode.location.left + 1,
-              screenY: textNode.location.top + 1
-            };
-            this.triggerReadMouseSelectedText(event, event);
-          });
+        // Sets the policy to disallow network voices.
+        this.setEnhancedNetworkVoicesPolicy(/* allowed= */ false);
+        assertFalse(selectToSpeak.prefsManager_.enhancedNetworkVoicesEnabled());
+      })]);
+      const textNode = this.findTextNode(root, 'This is some text');
+      const event = {
+        screenX: textNode.location.left + 1,
+        screenY: textNode.location.top + 1
+      };
+      this.triggerReadMouseSelectedText(event, event);
     });
 
 TEST_F(
     'SelectToSpeakEnhancedNetworkTtsVoicesTest',
-    'DisablesDialogIfDisallowedByPolicy', function() {
+    'DisablesDialogIfDisallowedByPolicy', async function() {
       this.setEnhancedNetworkVoicesPolicy(/* allowed= */ false);
 
-      this.runWithLoadedTree(
+      const root = await this.runWithLoadedTree(
           'data:text/html;charset=utf-8,' +
-              '<p>This is some text</p>',
-          function(root) {
-            assertFalse(this.mockTts.currentlySpeaking());
-            assertEquals(this.mockTts.pendingUtterances().length, 0);
-            this.mockTts.setOnSpeechCallbacks([this.newCallback(function(
-                utterance) {
-              // Dialog was not shown.
-              assertEquals(this.confirmationDialogShowCount_, 0);
-              assertFalse(
-                  selectToSpeak.prefsManager_.enhancedVoicesDialogShown());
+          '<p>This is some text</p>');
+      assertFalse(this.mockTts.currentlySpeaking());
+      assertEquals(this.mockTts.pendingUtterances().length, 0);
+      this.mockTts.setOnSpeechCallbacks([this.newCallback(function(utterance) {
+        // Dialog was not shown.
+        assertEquals(this.confirmationDialogShowCount_, 0);
+        assertFalse(selectToSpeak.prefsManager_.enhancedVoicesDialogShown());
 
-              // Speech proceeds without enhanced voices.
-              assertFalse(
-                  selectToSpeak.prefsManager_.enhancedNetworkVoicesEnabled());
-              assertTrue(this.mockTts.currentlySpeaking());
-              assertEquals(this.mockTts.pendingUtterances().length, 1);
-              this.assertEqualsCollapseWhitespace(
-                  this.mockTts.pendingUtterances()[0], 'This is some text');
-            })]);
-            const textNode = this.findTextNode(root, 'This is some text');
-            const event = {
-              screenX: textNode.location.left + 1,
-              screenY: textNode.location.top + 1
-            };
-            this.triggerReadMouseSelectedText(event, event);
-          });
+        // Speech proceeds without enhanced voices.
+        assertFalse(selectToSpeak.prefsManager_.enhancedNetworkVoicesEnabled());
+        assertTrue(this.mockTts.currentlySpeaking());
+        assertEquals(this.mockTts.pendingUtterances().length, 1);
+        this.assertEqualsCollapseWhitespace(
+            this.mockTts.pendingUtterances()[0], 'This is some text');
+      })]);
+      const textNode = this.findTextNode(root, 'This is some text');
+      const event = {
+        screenX: textNode.location.left + 1,
+        screenY: textNode.location.top + 1
+      };
+      this.triggerReadMouseSelectedText(event, event);
     });
diff --git a/chrome/browser/resources/chromeos/accessibility/select_to_speak/select_to_speak_keystroke_selection_test.js b/chrome/browser/resources/chromeos/accessibility/select_to_speak/select_to_speak_keystroke_selection_test.js
index 900e40c..6655e2b 100644
--- a/chrome/browser/resources/chromeos/accessibility/select_to_speak/select_to_speak_keystroke_selection_test.js
+++ b/chrome/browser/resources/chromeos/accessibility/select_to_speak/select_to_speak_keystroke_selection_test.js
@@ -39,8 +39,8 @@
    * @param {string} expected The expected string that will be read, ignoring
    *     extra whitespace, after this selection is triggered.
    */
-  testSimpleTextAtKeystroke(text, anchorOffset, focusOffset, expected) {
-    this.testReadTextAtKeystroke('<p>' + text + '</p>', function(root) {
+  async testSimpleTextAtKeystroke(text, anchorOffset, focusOffset, expected) {
+    await this.testReadTextAtKeystroke('<p>' + text + '</p>', function(root) {
       // Set the document selection. This will fire the changed event
       // above, allowing us to do the keystroke and test that speech
       // occurred properly.
@@ -69,22 +69,21 @@
    * @param {string} expected The expected string that will be read, ignoring
    *     extra whitespace, after this selection is triggered.
    */
-  testReadTextAtKeystroke(contents, setFocusCallback, expected) {
+  async testReadTextAtKeystroke(contents, setFocusCallback, expected) {
     setFocusCallback = this.newCallback(setFocusCallback);
-    this.runWithLoadedTree(contents, function(root) {
-      // Add an event listener that will start the user interaction
-      // of the test once the selection is completed.
-      root.addEventListener(
-          'documentSelectionChanged', this.newCallback(function(event) {
-            this.triggerReadSelectedText();
-            assertTrue(this.mockTts.currentlySpeaking());
-            assertEquals(this.mockTts.pendingUtterances().length, 1);
-            this.assertEqualsCollapseWhitespace(
-                this.mockTts.pendingUtterances()[0], expected);
-          }),
-          false);
-      setFocusCallback(root);
-    });
+    const root = await this.runWithLoadedTree(contents);
+    // Add an event listener that will start the user interaction
+    // of the test once the selection is completed.
+    root.addEventListener(
+        'documentSelectionChanged', this.newCallback(function(event) {
+          this.triggerReadSelectedText();
+          assertTrue(this.mockTts.currentlySpeaking());
+          assertEquals(this.mockTts.pendingUtterances().length, 1);
+          this.assertEqualsCollapseWhitespace(
+              this.mockTts.pendingUtterances()[0], expected);
+        }),
+        false);
+    setFocusCallback(root);
   }
 
   generateHtmlWithSelection(selectionCode, bodyHtml) {
@@ -101,34 +100,34 @@
 
 TEST_F(
     'SelectToSpeakKeystrokeSelectionTest', 'SpeaksTextAtKeystrokeFullText',
-    function() {
-      this.testSimpleTextAtKeystroke(
+    async function() {
+      await this.testSimpleTextAtKeystroke(
           'This is some text', 0, 17, 'This is some text');
     });
 
 TEST_F(
     'SelectToSpeakKeystrokeSelectionTest', 'SpeaksTextAtKeystrokePartialText',
-    function() {
-      this.testSimpleTextAtKeystroke(
+    async function() {
+      await this.testSimpleTextAtKeystroke(
           'This is some text', 0, 12, 'This is some');
     });
 
 TEST_F(
     'SelectToSpeakKeystrokeSelectionTest', 'SpeaksTextAtKeystrokeSingleWord',
-    function() {
-      this.testSimpleTextAtKeystroke('This is some text', 8, 12, 'some');
+    async function() {
+      await this.testSimpleTextAtKeystroke('This is some text', 8, 12, 'some');
     });
 
 TEST_F(
     'SelectToSpeakKeystrokeSelectionTest', 'SpeaksTextAtKeystrokePartialWord',
-    function() {
-      this.testSimpleTextAtKeystroke('This is some text', 8, 10, 'so');
+    async function() {
+      await this.testSimpleTextAtKeystroke('This is some text', 8, 10, 'so');
     });
 
 TEST_F(
     'SelectToSpeakKeystrokeSelectionTest', 'SpeaksAcrossNodesAtKeystroke',
-    function() {
-      this.testReadTextAtKeystroke(
+    async function() {
+      await this.testReadTextAtKeystroke(
           '<p>This is some <b>bold</b> text</p><p>Second paragraph</p>',
           function(root) {
             const firstNode = this.findTextNode(root, 'This is some ');
@@ -145,8 +144,8 @@
 
 TEST_F(
     'SelectToSpeakKeystrokeSelectionTest',
-    'SpeaksAcrossNodesSelectedBackwardsAtKeystroke', function() {
-      this.testReadTextAtKeystroke(
+    'SpeaksAcrossNodesSelectedBackwardsAtKeystroke', async function() {
+      await this.testReadTextAtKeystroke(
           '<p>This is some <b>bold</b> text</p><p>Second paragraph</p>',
           function(root) {
             // Set the document selection backwards in page order.
@@ -164,7 +163,7 @@
 
 TEST_F(
     'SelectToSpeakKeystrokeSelectionTest', 'SpeakTextSurroundedByBrs',
-    function() {
+    async function() {
       // If you load this html and double-click on "Selected text", this is the
       // document selection that occurs -- into the second <br/> element.
 
@@ -179,32 +178,31 @@
         });
       };
       setFocusCallback = this.newCallback(setFocusCallback);
-      this.runWithLoadedTree(
-          '<br/><p>Selected text</p><br/>', function(root) {
-            // Add an event listener that will start the user interaction
-            // of the test once the selection is completed.
-            root.addEventListener(
-                'documentSelectionChanged', this.newCallback(function(event) {
-                  this.triggerReadSelectedText();
-                  assertTrue(this.mockTts.currentlySpeaking());
-                  this.assertEqualsCollapseWhitespace(
-                      this.mockTts.pendingUtterances()[0], 'Selected text');
+      const root =
+          await this.runWithLoadedTree('<br/><p>Selected text</p><br/>');
+      // Add an event listener that will start the user interaction
+      // of the test once the selection is completed.
+      root.addEventListener(
+          'documentSelectionChanged', this.newCallback(function(event) {
+            this.triggerReadSelectedText();
+            assertTrue(this.mockTts.currentlySpeaking());
+            this.assertEqualsCollapseWhitespace(
+                this.mockTts.pendingUtterances()[0], 'Selected text');
 
-                  this.mockTts.finishPendingUtterance();
-                  if (this.mockTts.pendingUtterances().length === 1) {
-                    this.assertEqualsCollapseWhitespace(
-                        this.mockTts.pendingUtterances()[0], '');
-                  }
-                }),
-                false);
-            setFocusCallback(root);
-          });
+            this.mockTts.finishPendingUtterance();
+            if (this.mockTts.pendingUtterances().length === 1) {
+              this.assertEqualsCollapseWhitespace(
+                  this.mockTts.pendingUtterances()[0], '');
+            }
+          }),
+          false);
+      setFocusCallback(root);
     });
 
 TEST_F(
     'SelectToSpeakKeystrokeSelectionTest', 'StartsReadingAtFirstNodeWithText',
-    function() {
-      this.testReadTextAtKeystroke(
+    async function() {
+      await this.testReadTextAtKeystroke(
           '<div id="empty"></div><div><p>This is some <b>bold</b> text</p></div>',
           function(root) {
             const firstNode =
@@ -222,8 +220,8 @@
 
 TEST_F(
     'SelectToSpeakKeystrokeSelectionTest', 'IgnoresTextMarkedNotUserSelectable',
-    function() {
-      this.testReadTextAtKeystroke(
+    async function() {
+      await this.testReadTextAtKeystroke(
           '<div><p>This is some <span style="user-select:none">unselectable</span> text</p></div>',
           function(root) {
             const firstNode =
@@ -241,8 +239,8 @@
 
 TEST_F(
     'SelectToSpeakKeystrokeSelectionTest',
-    'HandlesSingleImageCorrectlyWithAutomation', function() {
-      this.testReadTextAtKeystroke(
+    'HandlesSingleImageCorrectlyWithAutomation', async function() {
+      await this.testReadTextAtKeystroke(
           '<img src="pipe.jpg" alt="one"/>', function(root) {
             const container = root.findAll({role: 'genericContainer'})[0];
             chrome.automation.setDocumentSelection({
@@ -256,8 +254,8 @@
 
 TEST_F(
     'SelectToSpeakKeystrokeSelectionTest',
-    'HandlesMultipleImagesCorrectlyWithAutomation', function() {
-      this.testReadTextAtKeystroke(
+    'HandlesMultipleImagesCorrectlyWithAutomation', async function() {
+      await this.testReadTextAtKeystroke(
           '<img src="pipe.jpg" alt="one"/>' +
               '<img src="pipe.jpg" alt="two"/><img src="pipe.jpg" alt="three"/>',
           function(root) {
@@ -274,104 +272,93 @@
 
 TEST_F(
     'SelectToSpeakKeystrokeSelectionTest',
-    'HandlesMultipleImagesCorrectlyWithJS1', function() {
+    'HandlesMultipleImagesCorrectlyWithJS1', async function() {
       // Using JS to do the selection instead of Automation, so that we can
       // ensure this is stable against changes in chrome.automation.
       const selectionCode =
           'let body = document.getElementsByTagName("body")[0];' +
           'range.setStart(body, 1);' +
           'range.setEnd(body, 2);';
-      this.runWithLoadedTree(
-          this.generateHtmlWithSelection(
-              selectionCode,
-              '<img id="one" src="pipe.jpg" alt="one"/>' +
-                  '<img id="two" src="pipe.jpg" alt="two"/>' +
-                  '<img id="three" src="pipe.jpg" alt="three"/>'),
-          function() {
-            this.triggerReadSelectedText();
-            assertTrue(this.mockTts.currentlySpeaking());
-            assertEquals(this.mockTts.pendingUtterances().length, 1);
-            this.assertEqualsCollapseWhitespace(
-                this.mockTts.pendingUtterances()[0], 'two');
-          });
+      await this.runWithLoadedTree(this.generateHtmlWithSelection(
+          selectionCode,
+          '<img id="one" src="pipe.jpg" alt="one"/>' +
+              '<img id="two" src="pipe.jpg" alt="two"/>' +
+              '<img id="three" src="pipe.jpg" alt="three"/>'));
+      this.triggerReadSelectedText();
+      assertTrue(this.mockTts.currentlySpeaking());
+      assertEquals(this.mockTts.pendingUtterances().length, 1);
+      this.assertEqualsCollapseWhitespace(
+          this.mockTts.pendingUtterances()[0], 'two');
     });
 
 TEST_F(
     'SelectToSpeakKeystrokeSelectionTest',
-    'HandlesMultipleImagesCorrectlyWithJS2', function() {
+    'HandlesMultipleImagesCorrectlyWithJS2', async function() {
       const selectionCode =
           'let body = document.getElementsByTagName("body")[0];' +
           'range.setStart(body, 1);' +
           'range.setEnd(body, 3);';
-      this.runWithLoadedTree(
-          this.generateHtmlWithSelection(
-              selectionCode,
-              '<img id="one" src="pipe.jpg" alt="one"/>' +
-                  '<img id="two" src="pipe.jpg" alt="two"/>' +
-                  '<img id="three" src="pipe.jpg" alt="three"/>'),
-          function() {
-            this.triggerReadSelectedText();
-            assertTrue(this.mockTts.currentlySpeaking());
-            assertEquals(this.mockTts.pendingUtterances().length, 1);
-            this.assertEqualsCollapseWhitespace(
-                this.mockTts.pendingUtterances()[0], 'two three');
-          });
+      await this.runWithLoadedTree(this.generateHtmlWithSelection(
+          selectionCode,
+          '<img id="one" src="pipe.jpg" alt="one"/>' +
+              '<img id="two" src="pipe.jpg" alt="two"/>' +
+              '<img id="three" src="pipe.jpg" alt="three"/>'));
+      this.triggerReadSelectedText();
+      assertTrue(this.mockTts.currentlySpeaking());
+      assertEquals(this.mockTts.pendingUtterances().length, 1);
+      this.assertEqualsCollapseWhitespace(
+          this.mockTts.pendingUtterances()[0], 'two three');
     });
 
 TEST_F(
     'SelectToSpeakKeystrokeSelectionTest', 'TextFieldFullySelected',
-    function() {
+    async function() {
       const selectionCode = 'let p = document.getElementsByTagName("p")[0];' +
           'let body = document.getElementsByTagName("body")[0];' +
           'range.setStart(p, 0);' +
           'range.setEnd(body, 2);';
-      this.runWithLoadedTree(
-          this.generateHtmlWithSelection(
-              selectionCode,
-              '<p>paragraph</p>' +
-                  '<input type="text" value="text field">'),
-          function() {
-            this.triggerReadSelectedText();
-            assertTrue(this.mockTts.currentlySpeaking());
-            assertEquals(this.mockTts.pendingUtterances().length, 1);
-            this.assertEqualsCollapseWhitespace(
-                this.mockTts.pendingUtterances()[0], 'paragraph');
+      await this.runWithLoadedTree(this.generateHtmlWithSelection(
+          selectionCode,
+          '<p>paragraph</p>' +
+              '<input type="text" value="text field">'));
+      this.triggerReadSelectedText();
+      assertTrue(this.mockTts.currentlySpeaking());
+      assertEquals(this.mockTts.pendingUtterances().length, 1);
+      this.assertEqualsCollapseWhitespace(
+          this.mockTts.pendingUtterances()[0], 'paragraph');
 
-            this.mockTts.finishPendingUtterance();
-            assertEquals(this.mockTts.pendingUtterances().length, 1);
-            this.assertEqualsCollapseWhitespace(
-                this.mockTts.pendingUtterances()[0], 'text field');
-          });
+      this.mockTts.finishPendingUtterance();
+      assertEquals(this.mockTts.pendingUtterances().length, 1);
+      this.assertEqualsCollapseWhitespace(
+          this.mockTts.pendingUtterances()[0], 'text field');
     });
 
 TEST_F(
     'SelectToSpeakKeystrokeSelectionTest', 'TwoTextFieldsFullySelected',
-    function() {
+    async function() {
       const selectionCode =
           'let body = document.getElementsByTagName("body")[0];' +
           'range.setStart(body, 0);' +
           'range.setEnd(body, 2);';
-      this.runWithLoadedTree(
-          this.generateHtmlWithSelection(
-              selectionCode,
-              '<input type="text" value="one"></input><textarea cols="5">two three</textarea>'),
-          function() {
-            this.triggerReadSelectedText();
-            assertTrue(this.mockTts.currentlySpeaking());
-            assertEquals(this.mockTts.pendingUtterances().length, 1);
-            this.assertEqualsCollapseWhitespace(
-                this.mockTts.pendingUtterances()[0], 'one');
+      await this.runWithLoadedTree(this.generateHtmlWithSelection(
+          selectionCode,
+          '<input type="text" value="one"></input>' +
+              '<textarea cols="5">two three</textarea>'));
+      this.triggerReadSelectedText();
+      assertTrue(this.mockTts.currentlySpeaking());
+      assertEquals(this.mockTts.pendingUtterances().length, 1);
+      this.assertEqualsCollapseWhitespace(
+          this.mockTts.pendingUtterances()[0], 'one');
 
-            this.mockTts.finishPendingUtterance();
-            assertEquals(this.mockTts.pendingUtterances().length, 1);
-            this.assertEqualsCollapseWhitespace(
-                this.mockTts.pendingUtterances()[0], 'two three');
-          });
+      this.mockTts.finishPendingUtterance();
+      assertEquals(this.mockTts.pendingUtterances().length, 1);
+      this.assertEqualsCollapseWhitespace(
+          this.mockTts.pendingUtterances()[0], 'two three');
     });
 
 TEST_F(
     'SelectToSpeakKeystrokeSelectionTest', 'TextInputPartiallySelected',
-    function() {
+    async function() {
       const html = '<script type="text/javascript">' +
           'function doSelection() {' +
           'let input = document.getElementById("input");' +
@@ -382,18 +369,17 @@
           '<body onload="doSelection()">' +
           '<input id="input" type="text" value="text field"></input>' +
           '</body>';
-      this.runWithLoadedTree(html, function() {
-        this.triggerReadSelectedText();
-        assertTrue(this.mockTts.currentlySpeaking());
-        assertEquals(this.mockTts.pendingUtterances().length, 1);
-        this.assertEqualsCollapseWhitespace(
-            this.mockTts.pendingUtterances()[0], 'field');
-      });
+      await this.runWithLoadedTree(html);
+      this.triggerReadSelectedText();
+      assertTrue(this.mockTts.currentlySpeaking());
+      assertEquals(this.mockTts.pendingUtterances().length, 1);
+      this.assertEqualsCollapseWhitespace(
+          this.mockTts.pendingUtterances()[0], 'field');
     });
 
 TEST_F(
     'SelectToSpeakKeystrokeSelectionTest', 'TextAreaPartiallySelected',
-    function() {
+    async function() {
       const html = '<script type="text/javascript">' +
           'function doSelection() {' +
           'let input = document.getElementById("input");' +
@@ -404,52 +390,49 @@
           '<body onload="doSelection()">' +
           '<textarea id="input" type="text" cols="10">first line second line</textarea>' +
           '</body>';
-      this.runWithLoadedTree(html, function() {
-        this.triggerReadSelectedText();
-        assertTrue(this.mockTts.currentlySpeaking());
-        assertEquals(this.mockTts.pendingUtterances().length, 1);
-        this.assertEqualsCollapseWhitespace(
-            this.mockTts.pendingUtterances()[0], 'line second');
-      });
+      await this.runWithLoadedTree(html);
+      this.triggerReadSelectedText();
+      assertTrue(this.mockTts.currentlySpeaking());
+      assertEquals(this.mockTts.pendingUtterances().length, 1);
+      this.assertEqualsCollapseWhitespace(
+          this.mockTts.pendingUtterances()[0], 'line second');
     });
 
-TEST_F('SelectToSpeakKeystrokeSelectionTest', 'HandlesTextWithBr', function() {
-  const selectionCode = 'let body = document.getElementsByTagName("body")[0];' +
-      'range.setStart(body, 0);' +
-      'range.setEnd(body, 3);';
-  this.runWithLoadedTree(
-      this.generateHtmlWithSelection(selectionCode, 'Test<br/><br/>Unread'),
-      function() {
-        this.triggerReadSelectedText();
-        assertTrue(this.mockTts.currentlySpeaking());
-        assertEquals(this.mockTts.pendingUtterances().length, 1);
-        this.assertEqualsCollapseWhitespace(
-            this.mockTts.pendingUtterances()[0], 'Test');
-      });
-});
+TEST_F(
+    'SelectToSpeakKeystrokeSelectionTest', 'HandlesTextWithBr',
+    async function() {
+      const selectionCode =
+          'let body = document.getElementsByTagName("body")[0];' +
+          'range.setStart(body, 0);' +
+          'range.setEnd(body, 3);';
+      await this.runWithLoadedTree(this.generateHtmlWithSelection(
+          selectionCode, 'Test<br/><br/>Unread'));
+      this.triggerReadSelectedText();
+      assertTrue(this.mockTts.currentlySpeaking());
+      assertEquals(this.mockTts.pendingUtterances().length, 1);
+      this.assertEqualsCollapseWhitespace(
+          this.mockTts.pendingUtterances()[0], 'Test');
+    });
 
 TEST_F(
     'SelectToSpeakKeystrokeSelectionTest', 'HandlesTextWithBrComplex',
-    function() {
+    async function() {
       const selectionCode = 'let p = document.getElementsByTagName("p")[0];' +
           'let body = document.getElementsByTagName("body")[0];' +
           'range.setStart(p, 0);' +
           'range.setEnd(body, 2);';
-      this.runWithLoadedTree(
-          this.generateHtmlWithSelection(
-              selectionCode, '<p>Some text</p><br/><br/>Unread'),
-          function() {
-            this.triggerReadSelectedText();
-            assertTrue(this.mockTts.currentlySpeaking());
-            assertEquals(this.mockTts.pendingUtterances().length, 1);
-            this.assertEqualsCollapseWhitespace(
-                this.mockTts.pendingUtterances()[0], 'Some text');
-          });
+      await this.runWithLoadedTree(this.generateHtmlWithSelection(
+          selectionCode, '<p>Some text</p><br/><br/>Unread'));
+      this.triggerReadSelectedText();
+      assertTrue(this.mockTts.currentlySpeaking());
+      assertEquals(this.mockTts.pendingUtterances().length, 1);
+      this.assertEqualsCollapseWhitespace(
+          this.mockTts.pendingUtterances()[0], 'Some text');
     });
 
 TEST_F(
     'SelectToSpeakKeystrokeSelectionTest', 'HandlesTextWithBrAfterText1',
-    function() {
+    async function() {
       // A bug was that if the selection was on the rootWebArea, paragraphs were
       // not counted correctly. The more divs and paragraphs before the
       // selection, the further off it got.
@@ -457,21 +440,18 @@
           'let body = document.getElementsByTagName("body")[0];' +
           'range.setStart(p, 1);' +
           'range.setEnd(body, 2);';
-      this.runWithLoadedTree(
-          this.generateHtmlWithSelection(
-              selectionCode, '<p>Unread</p><p>Some text</p><br/>Unread'),
-          function() {
-            this.triggerReadSelectedText();
-            assertTrue(this.mockTts.currentlySpeaking());
-            assertEquals(this.mockTts.pendingUtterances().length, 1);
-            this.assertEqualsCollapseWhitespace(
-                this.mockTts.pendingUtterances()[0], 'Some text');
-          });
+      await this.runWithLoadedTree(this.generateHtmlWithSelection(
+          selectionCode, '<p>Unread</p><p>Some text</p><br/>Unread'));
+      this.triggerReadSelectedText();
+      assertTrue(this.mockTts.currentlySpeaking());
+      assertEquals(this.mockTts.pendingUtterances().length, 1);
+      this.assertEqualsCollapseWhitespace(
+          this.mockTts.pendingUtterances()[0], 'Some text');
     });
 
 TEST_F(
     'SelectToSpeakKeystrokeSelectionTest', 'HandlesTextWithBrAfterText2',
-    function() {
+    async function() {
       // A bug was that if the selection was on the rootWebArea, paragraphs were
       // not counted correctly. The more divs and paragraphs before the
       // selection, the further off it got.
@@ -479,69 +459,61 @@
           'let body = document.getElementsByTagName("body")[0];' +
           'range.setStart(p, 1);' +
           'range.setEnd(body, 3);';
-      this.runWithLoadedTree(
-          this.generateHtmlWithSelection(
-              selectionCode, '<p>Unread</p><p>Some text</p><br/>Unread'),
-          function() {
-            this.triggerReadSelectedText();
-            assertTrue(this.mockTts.currentlySpeaking());
-            assertTrue(this.mockTts.pendingUtterances().length > 0);
-            this.assertEqualsCollapseWhitespace(
-                this.mockTts.pendingUtterances()[0], 'Some text');
+      await this.runWithLoadedTree(this.generateHtmlWithSelection(
+          selectionCode, '<p>Unread</p><p>Some text</p><br/>Unread'));
+      this.triggerReadSelectedText();
+      assertTrue(this.mockTts.currentlySpeaking());
+      assertTrue(this.mockTts.pendingUtterances().length > 0);
+      this.assertEqualsCollapseWhitespace(
+          this.mockTts.pendingUtterances()[0], 'Some text');
 
-            this.mockTts.finishPendingUtterance();
-            if (this.mockTts.pendingUtterances().length > 0) {
-              this.assertEqualsCollapseWhitespace(
-                  this.mockTts.pendingUtterances()[0], '');
-            }
-          });
+      this.mockTts.finishPendingUtterance();
+      if (this.mockTts.pendingUtterances().length > 0) {
+        this.assertEqualsCollapseWhitespace(
+            this.mockTts.pendingUtterances()[0], '');
+      }
     });
 
 TEST_F(
-    'SelectToSpeakKeystrokeSelectionTest', 'HandlesTextAreaAndBrs', function() {
+    'SelectToSpeakKeystrokeSelectionTest', 'HandlesTextAreaAndBrs',
+    async function() {
       const selectionCode =
           'let body = document.getElementsByTagName("body")[0];' +
           'range.setStart(body, 1);' +
           'range.setEnd(body, 4);';
-      this.runWithLoadedTree(
-          this.generateHtmlWithSelection(
-              selectionCode,
-              '<br/><br/><textarea>Some text</textarea><br/><br/>Unread'),
-          function() {
-            this.triggerReadSelectedText();
-            assertTrue(this.mockTts.currentlySpeaking());
-            assertEquals(this.mockTts.pendingUtterances().length, 1);
-            this.assertEqualsCollapseWhitespace(
-                this.mockTts.pendingUtterances()[0], 'Some text');
-          });
+      await this.runWithLoadedTree(this.generateHtmlWithSelection(
+          selectionCode,
+          '<br/><br/><textarea>Some text</textarea><br/><br/>Unread'));
+      this.triggerReadSelectedText();
+      assertTrue(this.mockTts.currentlySpeaking());
+      assertEquals(this.mockTts.pendingUtterances().length, 1);
+      this.assertEqualsCollapseWhitespace(
+          this.mockTts.pendingUtterances()[0], 'Some text');
     });
 
 TEST_F(
     'SelectToSpeakKeystrokeSelectionTest', 'textFieldWithComboBoxSimple',
-    function() {
+    async function() {
       const selectionCode =
           'let body = document.getElementsByTagName("body")[0];' +
           'range.setStart(body, 0);' +
           'range.setEnd(body, 1);';
-      this.runWithLoadedTree(
-          this.generateHtmlWithSelection(
-              selectionCode,
-              '<input list="list" value="one"></label><datalist id="list">' +
-                  '<option value="one"></datalist>'),
-          function() {
-            this.triggerReadSelectedText();
-            assertTrue(this.mockTts.currentlySpeaking());
-            assertEquals(this.mockTts.pendingUtterances().length, 1);
-            this.assertEqualsCollapseWhitespace(
-                this.mockTts.pendingUtterances()[0], 'one');
-          });
+      await this.runWithLoadedTree(this.generateHtmlWithSelection(
+          selectionCode,
+          '<input list="list" value="one"></label><datalist id="list">' +
+              '<option value="one"></datalist>'));
+      this.triggerReadSelectedText();
+      assertTrue(this.mockTts.currentlySpeaking());
+      assertEquals(this.mockTts.pendingUtterances().length, 1);
+      this.assertEqualsCollapseWhitespace(
+          this.mockTts.pendingUtterances()[0], 'one');
     });
 // TODO(katie): It doesn't seem possible to programatically specify a range that
 // selects only part of the text in a combo box.
 
 TEST_F(
     'SelectToSpeakKeystrokeSelectionTest', 'contentEditableInternallySelected',
-    function() {
+    async function() {
       const html = '<script type="text/javascript">' +
           'function doSelection() {' +
           'let input = document.getElementById("input");' +
@@ -559,145 +531,131 @@
           '<body onload="doSelection()">' +
           '<div id="input" contenteditable><p>a b c</p><p>d e f</p></div>' +
           '</body>';
-      this.runWithLoadedTree(html, function() {
-        this.triggerReadSelectedText();
-        assertTrue(this.mockTts.currentlySpeaking());
-        assertEquals(this.mockTts.pendingUtterances().length, 1);
-        this.assertEqualsCollapseWhitespace(
-            this.mockTts.pendingUtterances()[0], 'b c');
+      await this.runWithLoadedTree(html);
+      this.triggerReadSelectedText();
+      assertTrue(this.mockTts.currentlySpeaking());
+      assertEquals(this.mockTts.pendingUtterances().length, 1);
+      this.assertEqualsCollapseWhitespace(
+          this.mockTts.pendingUtterances()[0], 'b c');
 
-        this.mockTts.finishPendingUtterance();
-        assertEquals(this.mockTts.pendingUtterances().length, 1);
-        this.assertEqualsCollapseWhitespace(
-            this.mockTts.pendingUtterances()[0], 'd e');
-      });
+      this.mockTts.finishPendingUtterance();
+      assertEquals(this.mockTts.pendingUtterances().length, 1);
+      this.assertEqualsCollapseWhitespace(
+          this.mockTts.pendingUtterances()[0], 'd e');
     });
 
 TEST_F(
     'SelectToSpeakKeystrokeSelectionTest', 'contentEditableExternallySelected',
-    function() {
+    async function() {
       const selectionCode =
           'let body = document.getElementsByTagName("body")[0];' +
           'range.setStart(body, 1);' +
           'range.setEnd(body, 2);';
-      this.runWithLoadedTree(
-          this.generateHtmlWithSelection(
-              selectionCode,
-              'Unread <div id="input" contenteditable><p>a b c</p><p>d e f</p></div>' +
-                  ' Unread'),
-          function() {
-            this.triggerReadSelectedText();
-            assertTrue(this.mockTts.currentlySpeaking());
-            this.assertEqualsCollapseWhitespace(
-                this.mockTts.pendingUtterances()[0], 'a b c');
+      await this.runWithLoadedTree(this.generateHtmlWithSelection(
+          selectionCode,
+          'Unread <div id="input" contenteditable><p>a b c</p><p>d e f</p>' +
+              '</div> Unread'));
+      this.triggerReadSelectedText();
+      assertTrue(this.mockTts.currentlySpeaking());
+      this.assertEqualsCollapseWhitespace(
+          this.mockTts.pendingUtterances()[0], 'a b c');
 
-            this.mockTts.finishPendingUtterance();
-            this.assertEqualsCollapseWhitespace(
-                this.mockTts.pendingUtterances()[0], 'd e f');
-          });
+      this.mockTts.finishPendingUtterance();
+      this.assertEqualsCollapseWhitespace(
+          this.mockTts.pendingUtterances()[0], 'd e f');
     });
 
 TEST_F(
-    'SelectToSpeakKeystrokeSelectionTest', 'ReordersSvgSingleLine', function() {
+    'SelectToSpeakKeystrokeSelectionTest', 'ReordersSvgSingleLine',
+    async function() {
       const selectionCode =
           'let body = document.getElementsByTagName("body")[0];' +
           'range.setStart(body, 0);' +
           'range.setEnd(body, 1);';
-      this.runWithLoadedTree(
-          this.generateHtmlWithSelection(
-              selectionCode,
-              '<svg viewBox="0 0 240 80" xmlns="http://www.w3.org/2000/svg">' +
-                  '  <text x="65" y="55">Grumpy!</text>' +
-                  '  <text x="20" y="35">My</text>' +
-                  '  <text x="40" y="35">cat</text>' +
-                  '  <text x="55" y="55">is</text>' +
-                  '</svg>'),
-          function() {
-            this.triggerReadSelectedText();
-            assertTrue(this.mockTts.currentlySpeaking());
-            this.assertEqualsCollapseWhitespace(
-                this.mockTts.pendingUtterances()[0], 'My cat is Grumpy!');
-          });
+      await this.runWithLoadedTree(this.generateHtmlWithSelection(
+          selectionCode,
+          '<svg viewBox="0 0 240 80" xmlns="http://www.w3.org/2000/svg">' +
+              '  <text x="65" y="55">Grumpy!</text>' +
+              '  <text x="20" y="35">My</text>' +
+              '  <text x="40" y="35">cat</text>' +
+              '  <text x="55" y="55">is</text>' +
+              '</svg>'));
+      this.triggerReadSelectedText();
+      assertTrue(this.mockTts.currentlySpeaking());
+      this.assertEqualsCollapseWhitespace(
+          this.mockTts.pendingUtterances()[0], 'My cat is Grumpy!');
     });
 
 TEST_F(
-    'SelectToSpeakKeystrokeSelectionTest', 'ReordersSvgWithGroups', function() {
+    'SelectToSpeakKeystrokeSelectionTest', 'ReordersSvgWithGroups',
+    async function() {
       const selectionCode =
           'let body = document.getElementsByTagName("body")[0];' +
           'range.setStart(body, 0);' +
           'range.setEnd(body, 1);';
-      this.runWithLoadedTree(
-          this.generateHtmlWithSelection(
-              selectionCode,
-              '<svg viewBox="0 0 240 80" xmlns="http://www.w3.org/2000/svg">' +
-                  '  <g>' +
-                  '    <text x="65" y="0">Column 2, Text 1</text>' +
-                  '    <text x="65" y="50">Column 2, Text 2</text>' +
-                  '  </g>' +
-                  '  <g>' +
-                  '    <text x="0" y="50">Column 1, Text 2</text>' +
-                  '    <text x="0" y="0">Column 1, Text 1</text>' +
-                  '  </g>' +
-                  '</svg>'),
-          function() {
-            this.triggerReadSelectedText();
-            assertTrue(this.mockTts.currentlySpeaking());
-            this.assertEqualsCollapseWhitespace(
-                this.mockTts.pendingUtterances()[0], 'Column 1, Text 1');
+      await this.runWithLoadedTree(this.generateHtmlWithSelection(
+          selectionCode,
+          '<svg viewBox="0 0 240 80" xmlns="http://www.w3.org/2000/svg">' +
+              '  <g>' +
+              '    <text x="65" y="0">Column 2, Text 1</text>' +
+              '    <text x="65" y="50">Column 2, Text 2</text>' +
+              '  </g>' +
+              '  <g>' +
+              '    <text x="0" y="50">Column 1, Text 2</text>' +
+              '    <text x="0" y="0">Column 1, Text 1</text>' +
+              '  </g>' +
+              '</svg>'));
+      this.triggerReadSelectedText();
+      assertTrue(this.mockTts.currentlySpeaking());
+      this.assertEqualsCollapseWhitespace(
+          this.mockTts.pendingUtterances()[0], 'Column 1, Text 1');
 
-            this.mockTts.finishPendingUtterance();
-            this.assertEqualsCollapseWhitespace(
-                this.mockTts.pendingUtterances()[0], 'Column 1, Text 2');
+      this.mockTts.finishPendingUtterance();
+      this.assertEqualsCollapseWhitespace(
+          this.mockTts.pendingUtterances()[0], 'Column 1, Text 2');
 
-            this.mockTts.finishPendingUtterance();
-            this.assertEqualsCollapseWhitespace(
-                this.mockTts.pendingUtterances()[0], 'Column 2, Text 1');
+      this.mockTts.finishPendingUtterance();
+      this.assertEqualsCollapseWhitespace(
+          this.mockTts.pendingUtterances()[0], 'Column 2, Text 1');
 
-            this.mockTts.finishPendingUtterance();
-            this.assertEqualsCollapseWhitespace(
-                this.mockTts.pendingUtterances()[0], 'Column 2, Text 2');
-          });
+      this.mockTts.finishPendingUtterance();
+      this.assertEqualsCollapseWhitespace(
+          this.mockTts.pendingUtterances()[0], 'Column 2, Text 2');
     });
 TEST_F(
     'SelectToSpeakKeystrokeSelectionTest',
-    'NonReorderedSvgPreservesSelectionStartEnd', function() {
+    'NonReorderedSvgPreservesSelectionStartEnd', async function() {
       const selectionCode = 'const t1 = document.getElementById("t1");' +
           'const t2 = document.getElementById("t2");' +
           'range.setStart(t1.childNodes[0], 3);' +
           'range.setEnd(t2.childNodes[0], 2);';
-      this.runWithLoadedTree(
-          this.generateHtmlWithSelection(
-              selectionCode,
-              '<svg viewBox="0 0 240 80" xmlns="http://www.w3.org/2000/svg">' +
-                  '  <text id="t1" x="0" y="55">My cat</text>' +
-                  '  <text id="t2" x="100" y="55">is Grumpy!</text>' +
-                  '</svg>'),
-          function() {
-            this.triggerReadSelectedText();
-            assertTrue(this.mockTts.currentlySpeaking());
-            this.assertEqualsCollapseWhitespace(
-                this.mockTts.pendingUtterances()[0], 'cat is');
-          });
+      await this.runWithLoadedTree(this.generateHtmlWithSelection(
+          selectionCode,
+          '<svg viewBox="0 0 240 80" xmlns="http://www.w3.org/2000/svg">' +
+              '  <text id="t1" x="0" y="55">My cat</text>' +
+              '  <text id="t2" x="100" y="55">is Grumpy!</text>' +
+              '</svg>'));
+      this.triggerReadSelectedText();
+      assertTrue(this.mockTts.currentlySpeaking());
+      this.assertEqualsCollapseWhitespace(
+          this.mockTts.pendingUtterances()[0], 'cat is');
     });
 
 TEST_F(
     'SelectToSpeakKeystrokeSelectionTest',
-    'ReorderedSvgIgnoresSelectionStartEnd', function() {
+    'ReorderedSvgIgnoresSelectionStartEnd', async function() {
       const selectionCode = 'const t1 = document.getElementById("t1");' +
           'const t2 = document.getElementById("t2");' +
           'range.setStart(t1.childNodes[0], 3);' +
           'range.setEnd(t2.childNodes[0], 2);';
-      this.runWithLoadedTree(
-          this.generateHtmlWithSelection(
-              selectionCode,
-              '<svg viewBox="0 0 240 80" xmlns="http://www.w3.org/2000/svg">' +
-                  '  <text id="t1" x="100" y="55">is Grumpy!</text>' +
-                  '  <text id="t2" x="0" y="55">My cat</text>' +
-                  '</svg>'),
-          function() {
-            this.triggerReadSelectedText();
-            assertTrue(this.mockTts.currentlySpeaking());
-            this.assertEqualsCollapseWhitespace(
-                this.mockTts.pendingUtterances()[0], 'My cat is Grumpy!');
-          });
+      await this.runWithLoadedTree(this.generateHtmlWithSelection(
+          selectionCode,
+          '<svg viewBox="0 0 240 80" xmlns="http://www.w3.org/2000/svg">' +
+              '  <text id="t1" x="100" y="55">is Grumpy!</text>' +
+              '  <text id="t2" x="0" y="55">My cat</text>' +
+              '</svg>'));
+      this.triggerReadSelectedText();
+      assertTrue(this.mockTts.currentlySpeaking());
+      this.assertEqualsCollapseWhitespace(
+          this.mockTts.pendingUtterances()[0], 'My cat is Grumpy!');
     });
diff --git a/chrome/browser/resources/chromeos/accessibility/select_to_speak/select_to_speak_mouse_selection_test.js b/chrome/browser/resources/chromeos/accessibility/select_to_speak/select_to_speak_mouse_selection_test.js
index 8cbdf97..be8bc07 100644
--- a/chrome/browser/resources/chromeos/accessibility/select_to_speak/select_to_speak_mouse_selection_test.js
+++ b/chrome/browser/resources/chromeos/accessibility/select_to_speak/select_to_speak_mouse_selection_test.js
@@ -46,191 +46,176 @@
   }
 };
 
-TEST_F('SelectToSpeakMouseSelectionTest', 'SpeaksNodeWhenClicked', function() {
-  this.runWithLoadedTree(
-      'data:text/html;charset=utf-8,' +
-          '<p>This is some text</p>',
-      function(root) {
-        assertFalse(this.mockTts.currentlySpeaking());
-        assertEquals(this.mockTts.pendingUtterances().length, 0);
-        this.mockTts.setOnSpeechCallbacks(
-            [this.newCallback(function(utterance) {
-              // Speech starts asynchronously.
-              assertTrue(this.mockTts.currentlySpeaking());
-              assertEquals(this.mockTts.pendingUtterances().length, 1);
-              this.assertEqualsCollapseWhitespace(
-                  this.mockTts.pendingUtterances()[0], 'This is some text');
-            })]);
-        const textNode = this.findTextNode(root, 'This is some text');
-        const event = {
-          screenX: textNode.location.left + 1,
-          screenY: textNode.location.top + 1
-        };
-        this.triggerReadMouseSelectedText(event, event);
-      });
-});
+TEST_F(
+    'SelectToSpeakMouseSelectionTest', 'SpeaksNodeWhenClicked',
+    async function() {
+      const root = await this.runWithLoadedTree(
+          'data:text/html;charset=utf-8,' +
+          '<p>This is some text</p>');
+      assertFalse(this.mockTts.currentlySpeaking());
+      assertEquals(this.mockTts.pendingUtterances().length, 0);
+      this.mockTts.setOnSpeechCallbacks([this.newCallback(function(utterance) {
+        // Speech starts asynchronously.
+        assertTrue(this.mockTts.currentlySpeaking());
+        assertEquals(this.mockTts.pendingUtterances().length, 1);
+        this.assertEqualsCollapseWhitespace(
+            this.mockTts.pendingUtterances()[0], 'This is some text');
+      })]);
+      const textNode = this.findTextNode(root, 'This is some text');
+      const event = {
+        screenX: textNode.location.left + 1,
+        screenY: textNode.location.top + 1
+      };
+      this.triggerReadMouseSelectedText(event, event);
+    });
 
 TEST_F(
     'SelectToSpeakMouseSelectionTest', 'SpeaksMultipleNodesWhenDragged',
-    function() {
-      this.runWithLoadedTree(
+    async function() {
+      const root = await this.runWithLoadedTree(
           'data:text/html;charset=utf-8,' +
-              '<p>This is some text</p><p>This is some more text</p>',
-          function(root) {
-            assertFalse(this.mockTts.currentlySpeaking());
-            assertEquals(this.mockTts.pendingUtterances().length, 0);
-            this.mockTts.setOnSpeechCallbacks([
-              this.newCallback(function(utterance) {
-                assertTrue(this.mockTts.currentlySpeaking());
-                assertEquals(this.mockTts.pendingUtterances().length, 1);
-                this.assertEqualsCollapseWhitespace(
-                    utterance, 'This is some text');
-                this.mockTts.finishPendingUtterance();
-              }),
-              this.newCallback(function(utterance) {
-                this.assertEqualsCollapseWhitespace(
-                    utterance, 'This is some more text');
-              })
-            ]);
-            const firstNode = this.findTextNode(root, 'This is some text');
-            const downEvent = {
-              screenX: firstNode.location.left + 1,
-              screenY: firstNode.location.top + 1
-            };
-            const lastNode = this.findTextNode(root, 'This is some more text');
-            const upEvent = {
-              screenX: lastNode.location.left + lastNode.location.width,
-              screenY: lastNode.location.top + lastNode.location.height
-            };
-            this.triggerReadMouseSelectedText(downEvent, upEvent);
-          });
+          '<p>This is some text</p><p>This is some more text</p>');
+      assertFalse(this.mockTts.currentlySpeaking());
+      assertEquals(this.mockTts.pendingUtterances().length, 0);
+      this.mockTts.setOnSpeechCallbacks([
+        this.newCallback(function(utterance) {
+          assertTrue(this.mockTts.currentlySpeaking());
+          assertEquals(this.mockTts.pendingUtterances().length, 1);
+          this.assertEqualsCollapseWhitespace(utterance, 'This is some text');
+          this.mockTts.finishPendingUtterance();
+        }),
+        this.newCallback(function(utterance) {
+          this.assertEqualsCollapseWhitespace(
+              utterance, 'This is some more text');
+        })
+      ]);
+      const firstNode = this.findTextNode(root, 'This is some text');
+      const downEvent = {
+        screenX: firstNode.location.left + 1,
+        screenY: firstNode.location.top + 1
+      };
+      const lastNode = this.findTextNode(root, 'This is some more text');
+      const upEvent = {
+        screenX: lastNode.location.left + lastNode.location.width,
+        screenY: lastNode.location.top + lastNode.location.height
+      };
+      this.triggerReadMouseSelectedText(downEvent, upEvent);
     });
 
 TEST_F(
     'SelectToSpeakMouseSelectionTest', 'SpeaksAcrossNodesInAParagraph',
-    function() {
-      this.runWithLoadedTree(
+    async function() {
+      const root = await this.runWithLoadedTree(
           'data:text/html;charset=utf-8,' +
-              '<p style="width:200px">This is some text in a paragraph that wraps. ' +
-              '<i>Italic text</i></p>',
-          function(root) {
-            assertFalse(this.mockTts.currentlySpeaking());
-            assertEquals(this.mockTts.pendingUtterances().length, 0);
-            this.mockTts.setOnSpeechCallbacks(
-                [this.newCallback(function(utterance) {
-                  assertTrue(this.mockTts.currentlySpeaking());
-                  assertEquals(this.mockTts.pendingUtterances().length, 1);
-                  this.assertEqualsCollapseWhitespace(
-                      utterance,
-                      'This is some text in a paragraph that wraps. ' +
-                          'Italic text');
-                })]);
-            const firstNode = this.findTextNode(
-                root, 'This is some text in a paragraph that wraps. ');
-            const downEvent = {
-              screenX: firstNode.location.left + 1,
-              screenY: firstNode.location.top + 1
-            };
-            const lastNode = this.findTextNode(root, 'Italic text');
-            const upEvent = {
-              screenX: lastNode.location.left + lastNode.location.width,
-              screenY: lastNode.location.top + lastNode.location.height
-            };
-            this.triggerReadMouseSelectedText(downEvent, upEvent);
-          });
+          '<p style="width:200px">This is some text in a paragraph that ' +
+          'wraps. <i>Italic text</i></p>');
+      assertFalse(this.mockTts.currentlySpeaking());
+      assertEquals(this.mockTts.pendingUtterances().length, 0);
+      this.mockTts.setOnSpeechCallbacks([this.newCallback(function(utterance) {
+        assertTrue(this.mockTts.currentlySpeaking());
+        assertEquals(this.mockTts.pendingUtterances().length, 1);
+        this.assertEqualsCollapseWhitespace(
+            utterance,
+            'This is some text in a paragraph that wraps. ' +
+                'Italic text');
+      })]);
+      const firstNode = this.findTextNode(
+          root, 'This is some text in a paragraph that wraps. ');
+      const downEvent = {
+        screenX: firstNode.location.left + 1,
+        screenY: firstNode.location.top + 1
+      };
+      const lastNode = this.findTextNode(root, 'Italic text');
+      const upEvent = {
+        screenX: lastNode.location.left + lastNode.location.width,
+        screenY: lastNode.location.top + lastNode.location.height
+      };
+      this.triggerReadMouseSelectedText(downEvent, upEvent);
     });
 
 TEST_F(
     'SelectToSpeakMouseSelectionTest', 'SpeaksNodeAfterTrayTapAndMouseClick',
-    function() {
-      this.runWithLoadedTree(
+    async function() {
+      const root = await this.runWithLoadedTree(
           'data:text/html;charset=utf-8,' +
-              '<p>This is some text</p>',
-          function(root) {
-            assertFalse(this.mockTts.currentlySpeaking());
-            this.mockTts.setOnSpeechCallbacks(
-                [this.newCallback(function(utterance) {
-                  // Speech starts asynchronously.
-                  assertTrue(this.mockTts.currentlySpeaking());
-                  assertEquals(this.mockTts.pendingUtterances().length, 1);
-                  this.assertEqualsCollapseWhitespace(
-                      this.mockTts.pendingUtterances()[0], 'This is some text');
-                })]);
+          '<p>This is some text</p>');
+      assertFalse(this.mockTts.currentlySpeaking());
+      this.mockTts.setOnSpeechCallbacks([this.newCallback(function(utterance) {
+        // Speech starts asynchronously.
+        assertTrue(this.mockTts.currentlySpeaking());
+        assertEquals(this.mockTts.pendingUtterances().length, 1);
+        this.assertEqualsCollapseWhitespace(
+            this.mockTts.pendingUtterances()[0], 'This is some text');
+      })]);
 
-            const textNode = this.findTextNode(root, 'This is some text');
-            const event = {
-              screenX: textNode.location.left + 1,
-              screenY: textNode.location.top + 1
-            };
-            // A state change request should shift us into 'selecting' state
-            // from 'inactive'.
-            const desktop = root.parent.root;
-            this.tapTrayButton(desktop, () => {
-              selectToSpeak.fireMockMouseDownEvent(event);
-              selectToSpeak.fireMockMouseUpEvent(event);
-            });
-          });
+      const textNode = this.findTextNode(root, 'This is some text');
+      const event = {
+        screenX: textNode.location.left + 1,
+        screenY: textNode.location.top + 1
+      };
+      // A state change request should shift us into 'selecting' state
+      // from 'inactive'.
+      const desktop = root.parent.root;
+      this.tapTrayButton(desktop, () => {
+        selectToSpeak.fireMockMouseDownEvent(event);
+        selectToSpeak.fireMockMouseUpEvent(event);
+      });
     });
 
 TEST_F(
     'SelectToSpeakMouseSelectionTest', 'CancelsSelectionModeWithStateChange',
-    function() {
-      this.runWithLoadedTree(
+    async function() {
+      const root = await this.runWithLoadedTree(
           'data:text/html;charset=utf-8,' +
-              '<p>This is some text</p>',
-          function(root) {
-            const textNode = this.findTextNode(root, 'This is some text');
-            const event = {
-              screenX: textNode.location.left + 1,
-              screenY: textNode.location.top + 1
-            };
-            // A state change request should shift us into 'selecting' state
-            // from 'inactive'.
-            const desktop = root.parent.root;
-            this.tapTrayButton(desktop, () => {
-              selectToSpeak.fireMockMouseDownEvent(event);
-              assertEquals(SelectToSpeakState.SELECTING, selectToSpeak.state_);
+          '<p>This is some text</p>');
+      const textNode = this.findTextNode(root, 'This is some text');
+      const event = {
+        screenX: textNode.location.left + 1,
+        screenY: textNode.location.top + 1
+      };
+      // A state change request should shift us into 'selecting' state
+      // from 'inactive'.
+      const desktop = root.parent.root;
+      this.tapTrayButton(desktop, () => {
+        selectToSpeak.fireMockMouseDownEvent(event);
+        assertEquals(SelectToSpeakState.SELECTING, selectToSpeak.state_);
 
-              // Another state change puts us back in 'inactive'.
-              this.tapTrayButton(desktop, () => {
-                assertEquals(SelectToSpeakState.INACTIVE, selectToSpeak.state_);
-              });
-            });
-          });
+        // Another state change puts us back in 'inactive'.
+        this.tapTrayButton(desktop, () => {
+          assertEquals(SelectToSpeakState.INACTIVE, selectToSpeak.state_);
+        });
+      });
     });
 
 TEST_F(
-    'SelectToSpeakMouseSelectionTest', 'CancelsSpeechWithTrayTap', function() {
-      this.runWithLoadedTree(
+    'SelectToSpeakMouseSelectionTest', 'CancelsSpeechWithTrayTap',
+    async function() {
+      const root = await this.runWithLoadedTree(
           'data:text/html;charset=utf-8,' +
-              '<p>This is some text</p>',
-          function(root) {
-            assertFalse(this.mockTts.currentlySpeaking());
-            assertEquals(this.mockTts.pendingUtterances().length, 0);
-            this.mockTts.setOnSpeechCallbacks(
-                [this.newCallback(function(utterance) {
-                  // Speech starts asynchronously.
-                  assertTrue(this.mockTts.currentlySpeaking());
-                  assertEquals(this.mockTts.pendingUtterances().length, 1);
-                  this.assertEqualsCollapseWhitespace(
-                      this.mockTts.pendingUtterances()[0], 'This is some text');
+          '<p>This is some text</p>');
+      assertFalse(this.mockTts.currentlySpeaking());
+      assertEquals(this.mockTts.pendingUtterances().length, 0);
+      this.mockTts.setOnSpeechCallbacks([this.newCallback(function(utterance) {
+        // Speech starts asynchronously.
+        assertTrue(this.mockTts.currentlySpeaking());
+        assertEquals(this.mockTts.pendingUtterances().length, 1);
+        this.assertEqualsCollapseWhitespace(
+            this.mockTts.pendingUtterances()[0], 'This is some text');
 
-                  // Cancel speech and make sure state resets to INACTIVE.
-                  const desktop = root.parent.root;
-                  this.tapTrayButton(desktop, () => {
-                    assertFalse(this.mockTts.currentlySpeaking());
-                    assertEquals(this.mockTts.pendingUtterances().length, 0);
-                    assertEquals(
-                        SelectToSpeakState.INACTIVE, selectToSpeak.state_);
-                  });
-                })]);
-            const textNode = this.findTextNode(root, 'This is some text');
-            const event = {
-              screenX: textNode.location.left + 1,
-              screenY: textNode.location.top + 1
-            };
-            this.triggerReadMouseSelectedText(event, event);
-          });
+        // Cancel speech and make sure state resets to INACTIVE.
+        const desktop = root.parent.root;
+        this.tapTrayButton(desktop, () => {
+          assertFalse(this.mockTts.currentlySpeaking());
+          assertEquals(this.mockTts.pendingUtterances().length, 0);
+          assertEquals(SelectToSpeakState.INACTIVE, selectToSpeak.state_);
+        });
+      })]);
+      const textNode = this.findTextNode(root, 'This is some text');
+      const event = {
+        screenX: textNode.location.left + 1,
+        screenY: textNode.location.top + 1
+      };
+      this.triggerReadMouseSelectedText(event, event);
     });
 
 // TODO(crbug.com/1177140) Re-enable test
diff --git a/chrome/browser/resources/chromeos/accessibility/select_to_speak/select_to_speak_navigation_control_test.js b/chrome/browser/resources/chromeos/accessibility/select_to_speak/select_to_speak_navigation_control_test.js
index 46b410a..2af2df5 100644
--- a/chrome/browser/resources/chromeos/accessibility/select_to_speak/select_to_speak_navigation_control_test.js
+++ b/chrome/browser/resources/chromeos/accessibility/select_to_speak/select_to_speak_navigation_control_test.js
@@ -76,76 +76,76 @@
 
 TEST_F(
     'SelectToSpeakNavigationControlTest', 'NavigatesToNextParagraph',
-    function() {
+    async function() {
       const bodyHtml = `
     <p id="p1">Paragraph 1</p>
     <p id="p2">Paragraph 2</p>'
   `;
-      this.runWithLoadedTree(
-          this.generateHtmlWithSelectedElement('p1', bodyHtml), () => {
-            this.triggerReadSelectedText();
+      await this.runWithLoadedTree(
+          this.generateHtmlWithSelectedElement('p1', bodyHtml));
+      this.triggerReadSelectedText();
 
-            // Speaks first paragraph
-            assertTrue(this.mockTts.currentlySpeaking());
-            assertEquals(this.mockTts.pendingUtterances().length, 1);
-            this.assertEqualsCollapseWhitespace(
-                this.mockTts.pendingUtterances()[0], 'Paragraph 1');
+      // Speaks first paragraph
+      assertTrue(this.mockTts.currentlySpeaking());
+      assertEquals(this.mockTts.pendingUtterances().length, 1);
+      this.assertEqualsCollapseWhitespace(
+          this.mockTts.pendingUtterances()[0], 'Paragraph 1');
 
-            // TODO(joelriley@google.com): Figure out a better way to trigger
-            // the actual floating panel button rather than calling private
-            // method directly.
-            selectToSpeak.onNextParagraphRequested();
+      // TODO(joelriley@google.com): Figure out a better way to trigger
+      // the actual floating panel button rather than calling private
+      // method directly.
+      selectToSpeak.onNextParagraphRequested();
 
-            // Speaks second paragraph
-            this.waitOneEventLoop(() => {
-              assertTrue(this.mockTts.currentlySpeaking());
-              assertEquals(this.mockTts.pendingUtterances().length, 1);
-              this.assertEqualsCollapseWhitespace(
-                  this.mockTts.pendingUtterances()[0], 'Paragraph 2');
-            });
-          });
+      // Speaks second paragraph
+      this.waitOneEventLoop(() => {
+        assertTrue(this.mockTts.currentlySpeaking());
+        assertEquals(this.mockTts.pendingUtterances().length, 1);
+        this.assertEqualsCollapseWhitespace(
+            this.mockTts.pendingUtterances()[0], 'Paragraph 2');
+      });
     });
 
 TEST_F(
     'SelectToSpeakNavigationControlTest', 'NavigatesToPreviousParagraph',
-    function() {
+    async function() {
       const bodyHtml = `
     <p id="p1">Paragraph 1</p>
     <p id="p2">Paragraph 2</p>'
   `;
-      this.runWithLoadedTree(
-          this.generateHtmlWithSelectedElement('p2', bodyHtml), () => {
-            this.triggerReadSelectedText();
+      await this.runWithLoadedTree(
+          this.generateHtmlWithSelectedElement('p2', bodyHtml));
+      this.triggerReadSelectedText();
 
-            // Speaks first paragraph
-            assertTrue(this.mockTts.currentlySpeaking());
-            assertEquals(this.mockTts.pendingUtterances().length, 1);
-            this.assertEqualsCollapseWhitespace(
-                this.mockTts.pendingUtterances()[0], 'Paragraph 2');
+      // Speaks first paragraph
+      assertTrue(this.mockTts.currentlySpeaking());
+      assertEquals(this.mockTts.pendingUtterances().length, 1);
+      this.assertEqualsCollapseWhitespace(
+          this.mockTts.pendingUtterances()[0], 'Paragraph 2');
 
-            // TODO(joelriley@google.com): Figure out a better way to trigger
-            // the actual floating panel button rather than calling private
-            // method directly.
-            selectToSpeak.onPreviousParagraphRequested();
+      // TODO(joelriley@google.com): Figure out a better way to trigger
+      // the actual floating panel button rather than calling private
+      // method directly.
+      selectToSpeak.onPreviousParagraphRequested();
 
-            // Speaks second paragraph
-            this.waitOneEventLoop(() => {
-              assertTrue(this.mockTts.currentlySpeaking());
-              assertEquals(this.mockTts.pendingUtterances().length, 1);
-              this.assertEqualsCollapseWhitespace(
-                  this.mockTts.pendingUtterances()[0], 'Paragraph 1');
-            });
-          });
+      // Speaks second paragraph
+      this.waitOneEventLoop(() => {
+        assertTrue(this.mockTts.currentlySpeaking());
+        assertEquals(this.mockTts.pendingUtterances().length, 1);
+        this.assertEqualsCollapseWhitespace(
+            this.mockTts.pendingUtterances()[0], 'Paragraph 1');
+      });
     });
 
 TEST_F(
-    'SelectToSpeakNavigationControlTest', 'ReadsParagraphOnClick', function() {
+    'SelectToSpeakNavigationControlTest', 'ReadsParagraphOnClick',
+    async function() {
       const bodyHtml = `
       <p id="p1">Sentence <span>one</span>. Sentence two.</p>
       <p id="p2">Paragraph <span>two</span></p>'
     `;
-      this.runWithLoadedTree(bodyHtml, (root) => {
-        this.mockTts.setOnSpeechCallbacks([this.newCallback((utterance) => {
+      const root = await this.runWithLoadedTree(bodyHtml);
+      this.mockTts.setOnSpeechCallbacks([
+        this.newCallback((utterance) => {
           // Speech for first click.
           assertTrue(this.mockTts.currentlySpeaking());
           assertEquals(this.mockTts.pendingUtterances().length, 1);
@@ -168,120 +168,116 @@
             screenY: textNode2.location.top + 1
           };
           this.triggerReadMouseSelectedText(mouseEvent2, mouseEvent2);
-        })]);
+        })
+      ]);
 
-        // Click on node in first paragraph.
-        const textNode1 = this.findTextNode(root, 'one');
-        const event1 = {
-          screenX: textNode1.location.left + 1,
-          screenY: textNode1.location.top + 1
-        };
-        this.triggerReadMouseSelectedText(event1, event1);
-      });
+      // Click on node in first paragraph.
+      const textNode1 = this.findTextNode(root, 'one');
+      const event1 = {
+        screenX: textNode1.location.left + 1,
+        screenY: textNode1.location.top + 1
+      };
+      this.triggerReadMouseSelectedText(event1, event1);
     });
 
 TEST_F(
     'SelectToSpeakNavigationControlTest', 'PauseResumeWithinTheSentence',
-    function() {
+    async function() {
       const bodyHtml = `
       <p id="p1">First sentence. Second sentence. Third sentence.</p>'
     `;
-      this.runWithLoadedTree(
-          this.generateHtmlWithSelectedElement('p1', bodyHtml), () => {
-            this.triggerReadSelectedText();
+      await this.runWithLoadedTree(
+          this.generateHtmlWithSelectedElement('p1', bodyHtml));
+      this.triggerReadSelectedText();
 
-            // Speaks until the second word of the second sentence.
-            this.mockTts.speakUntilCharIndex(23);
-            assertTrue(this.mockTts.currentlySpeaking());
-            assertEquals(this.mockTts.pendingUtterances().length, 1);
-            this.assertEqualsCollapseWhitespace(
-                this.mockTts.pendingUtterances()[0],
-                'First sentence. Second sentence. Third sentence.');
+      // Speaks until the second word of the second sentence.
+      this.mockTts.speakUntilCharIndex(23);
+      assertTrue(this.mockTts.currentlySpeaking());
+      assertEquals(this.mockTts.pendingUtterances().length, 1);
+      this.assertEqualsCollapseWhitespace(
+          this.mockTts.pendingUtterances()[0],
+          'First sentence. Second sentence. Third sentence.');
 
-            // Hitting pause will stop the current TTS.
-            selectToSpeak.onPauseRequested();
-            assertFalse(this.mockTts.currentlySpeaking());
-            assertEquals(this.mockTts.pendingUtterances().length, 0);
+      // Hitting pause will stop the current TTS.
+      selectToSpeak.onPauseRequested();
+      assertFalse(this.mockTts.currentlySpeaking());
+      assertEquals(this.mockTts.pendingUtterances().length, 0);
 
-            // Hitting resume will start from the remaining content of the
-            // second sentence.
-            selectToSpeak.onResumeRequested();
-            assertTrue(this.mockTts.currentlySpeaking());
-            assertEquals(this.mockTts.pendingUtterances().length, 1);
-            this.assertEqualsCollapseWhitespace(
-                this.mockTts.pendingUtterances()[0],
-                'sentence. Third sentence.');
-          });
+      // Hitting resume will start from the remaining content of the
+      // second sentence.
+      selectToSpeak.onResumeRequested();
+      assertTrue(this.mockTts.currentlySpeaking());
+      assertEquals(this.mockTts.pendingUtterances().length, 1);
+      this.assertEqualsCollapseWhitespace(
+          this.mockTts.pendingUtterances()[0], 'sentence. Third sentence.');
     });
 
 TEST_F(
     'SelectToSpeakNavigationControlTest', 'PauseResumeAtTheBeginningOfSentence',
-    function() {
+    async function() {
       const bodyHtml = `
       <p id="p1">First sentence. Second sentence. Third sentence.</p>'
     `;
-      this.runWithLoadedTree(
-          this.generateHtmlWithSelectedElement('p1', bodyHtml), () => {
-            this.triggerReadSelectedText();
+      await this.runWithLoadedTree(
+          this.generateHtmlWithSelectedElement('p1', bodyHtml));
+      this.triggerReadSelectedText();
 
-            // Speaks until the third sentence.
-            this.mockTts.speakUntilCharIndex(33);
-            assertTrue(this.mockTts.currentlySpeaking());
-            assertEquals(this.mockTts.pendingUtterances().length, 1);
-            this.assertEqualsCollapseWhitespace(
-                this.mockTts.pendingUtterances()[0],
-                'First sentence. Second sentence. Third sentence.');
+      // Speaks until the third sentence.
+      this.mockTts.speakUntilCharIndex(33);
+      assertTrue(this.mockTts.currentlySpeaking());
+      assertEquals(this.mockTts.pendingUtterances().length, 1);
+      this.assertEqualsCollapseWhitespace(
+          this.mockTts.pendingUtterances()[0],
+          'First sentence. Second sentence. Third sentence.');
 
-            // Hitting pause will stop the current TTS.
-            selectToSpeak.onPauseRequested();
-            assertFalse(this.mockTts.currentlySpeaking());
-            assertEquals(this.mockTts.pendingUtterances().length, 0);
+      // Hitting pause will stop the current TTS.
+      selectToSpeak.onPauseRequested();
+      assertFalse(this.mockTts.currentlySpeaking());
+      assertEquals(this.mockTts.pendingUtterances().length, 0);
 
-            // Hitting resume will start from the beginning of the third
-            // sentence.
-            selectToSpeak.onResumeRequested();
-            assertTrue(this.mockTts.currentlySpeaking());
-            assertEquals(this.mockTts.pendingUtterances().length, 1);
-            this.assertEqualsCollapseWhitespace(
-                this.mockTts.pendingUtterances()[0], 'Third sentence.');
-          });
+      // Hitting resume will start from the beginning of the third
+      // sentence.
+      selectToSpeak.onResumeRequested();
+      assertTrue(this.mockTts.currentlySpeaking());
+      assertEquals(this.mockTts.pendingUtterances().length, 1);
+      this.assertEqualsCollapseWhitespace(
+          this.mockTts.pendingUtterances()[0], 'Third sentence.');
     });
 
 TEST_F(
     'SelectToSpeakNavigationControlTest',
-    'PauseResumeAtTheBeginningOfParagraph', function() {
+    'PauseResumeAtTheBeginningOfParagraph', async function() {
       const bodyHtml = `
       <p id="p1">first sentence.</p>'
     `;
-      this.runWithLoadedTree(
-          this.generateHtmlWithSelectedElement('p1', bodyHtml), () => {
-            this.triggerReadSelectedText();
+      await this.runWithLoadedTree(
+          this.generateHtmlWithSelectedElement('p1', bodyHtml));
+      this.triggerReadSelectedText();
 
-            // Speaks until the second word.
-            this.mockTts.speakUntilCharIndex(6);
-            assertTrue(this.mockTts.currentlySpeaking());
-            assertEquals(this.mockTts.pendingUtterances().length, 1);
-            this.assertEqualsCollapseWhitespace(
-                this.mockTts.pendingUtterances()[0], 'first sentence.');
+      // Speaks until the second word.
+      this.mockTts.speakUntilCharIndex(6);
+      assertTrue(this.mockTts.currentlySpeaking());
+      assertEquals(this.mockTts.pendingUtterances().length, 1);
+      this.assertEqualsCollapseWhitespace(
+          this.mockTts.pendingUtterances()[0], 'first sentence.');
 
-            // Hitting pause will stop the current TTS.
-            selectToSpeak.onPauseRequested();
-            assertFalse(this.mockTts.currentlySpeaking());
-            assertEquals(this.mockTts.pendingUtterances().length, 0);
+      // Hitting pause will stop the current TTS.
+      selectToSpeak.onPauseRequested();
+      assertFalse(this.mockTts.currentlySpeaking());
+      assertEquals(this.mockTts.pendingUtterances().length, 0);
 
-            // Hitting resume will start from the remaining content of the
-            // paragraph.
-            selectToSpeak.onResumeRequested();
-            assertTrue(this.mockTts.currentlySpeaking());
-            assertEquals(this.mockTts.pendingUtterances().length, 1);
-            this.assertEqualsCollapseWhitespace(
-                this.mockTts.pendingUtterances()[0], 'sentence.');
-          });
+      // Hitting resume will start from the remaining content of the
+      // paragraph.
+      selectToSpeak.onResumeRequested();
+      assertTrue(this.mockTts.currentlySpeaking());
+      assertEquals(this.mockTts.pendingUtterances().length, 1);
+      this.assertEqualsCollapseWhitespace(
+          this.mockTts.pendingUtterances()[0], 'sentence.');
     });
 
 TEST_F(
     'SelectToSpeakNavigationControlTest',
-    'PauseResumeInTheMiddleOfMultiParagraphs', function() {
+    'PauseResumeInTheMiddleOfMultiParagraphs', async function() {
       const bodyHtml = `
       <span id='s1'>
         <p>Paragraph one.</p>
@@ -289,47 +285,46 @@
         <p>Paragraph three.</p>
       </span>'
       `;
-      this.runWithLoadedTree(
-          this.generateHtmlWithSelectedElement('s1', bodyHtml), () => {
-            this.triggerReadSelectedText();
+      await this.runWithLoadedTree(
+          this.generateHtmlWithSelectedElement('s1', bodyHtml));
+      this.triggerReadSelectedText();
 
-            // Speaks until the second word.
-            this.mockTts.speakUntilCharIndex(10);
-            assertTrue(this.mockTts.currentlySpeaking());
-            assertEquals(this.mockTts.pendingUtterances().length, 1);
-            this.assertEqualsCollapseWhitespace(
-                this.mockTts.pendingUtterances()[0], 'Paragraph one.');
+      // Speaks until the second word.
+      this.mockTts.speakUntilCharIndex(10);
+      assertTrue(this.mockTts.currentlySpeaking());
+      assertEquals(this.mockTts.pendingUtterances().length, 1);
+      this.assertEqualsCollapseWhitespace(
+          this.mockTts.pendingUtterances()[0], 'Paragraph one.');
 
-            // Hitting pause will stop the current TTS.
-            selectToSpeak.onPauseRequested();
-            assertFalse(this.mockTts.currentlySpeaking());
-            assertEquals(this.mockTts.pendingUtterances().length, 0);
+      // Hitting pause will stop the current TTS.
+      selectToSpeak.onPauseRequested();
+      assertFalse(this.mockTts.currentlySpeaking());
+      assertEquals(this.mockTts.pendingUtterances().length, 0);
 
-            // Hitting resume will start from the remaining content of the
-            // paragraph.
-            selectToSpeak.onResumeRequested();
-            assertTrue(this.mockTts.currentlySpeaking());
-            assertEquals(this.mockTts.pendingUtterances().length, 1);
-            this.assertEqualsCollapseWhitespace(
-                this.mockTts.pendingUtterances()[0], 'one.');
+      // Hitting resume will start from the remaining content of the
+      // paragraph.
+      selectToSpeak.onResumeRequested();
+      assertTrue(this.mockTts.currentlySpeaking());
+      assertEquals(this.mockTts.pendingUtterances().length, 1);
+      this.assertEqualsCollapseWhitespace(
+          this.mockTts.pendingUtterances()[0], 'one.');
 
-            // Keep reading will finish all the content.
-            this.mockTts.finishPendingUtterance();
-            assertTrue(this.mockTts.currentlySpeaking());
-            assertEquals(this.mockTts.pendingUtterances().length, 1);
-            this.assertEqualsCollapseWhitespace(
-                this.mockTts.pendingUtterances()[0], 'Paragraph two.');
-            this.mockTts.finishPendingUtterance();
-            assertTrue(this.mockTts.currentlySpeaking());
-            assertEquals(this.mockTts.pendingUtterances().length, 1);
-            this.assertEqualsCollapseWhitespace(
-                this.mockTts.pendingUtterances()[0], 'Paragraph three.');
-          });
+      // Keep reading will finish all the content.
+      this.mockTts.finishPendingUtterance();
+      assertTrue(this.mockTts.currentlySpeaking());
+      assertEquals(this.mockTts.pendingUtterances().length, 1);
+      this.assertEqualsCollapseWhitespace(
+          this.mockTts.pendingUtterances()[0], 'Paragraph two.');
+      this.mockTts.finishPendingUtterance();
+      assertTrue(this.mockTts.currentlySpeaking());
+      assertEquals(this.mockTts.pendingUtterances().length, 1);
+      this.assertEqualsCollapseWhitespace(
+          this.mockTts.pendingUtterances()[0], 'Paragraph three.');
     });
 
 TEST_F(
     'SelectToSpeakNavigationControlTest', 'PauseResumeAfterParagraphNavigation',
-    function() {
+    async function() {
       const bodyHtml = `
       <span id='s1'>
         <p>Paragraph one.</p>
@@ -337,113 +332,107 @@
         <p>Paragraph three.</p>
       </span>'
       `;
-      this.runWithLoadedTree(
-          this.generateHtmlWithSelectedElement('s1', bodyHtml),
-          async function() {
-            this.triggerReadSelectedText();
+      await this.runWithLoadedTree(
+          this.generateHtmlWithSelectedElement('s1', bodyHtml));
+      this.triggerReadSelectedText();
 
-            // Navigates to the next paragraph and speaks until the second word.
-            await selectToSpeak.onNextParagraphRequested();
-            this.mockTts.speakUntilCharIndex(10);
-            assertTrue(this.mockTts.currentlySpeaking());
-            assertEquals(this.mockTts.pendingUtterances().length, 1);
-            this.assertEqualsCollapseWhitespace(
-                this.mockTts.pendingUtterances()[0], 'Paragraph two.');
+      // Navigates to the next paragraph and speaks until the second word.
+      await selectToSpeak.onNextParagraphRequested();
+      this.mockTts.speakUntilCharIndex(10);
+      assertTrue(this.mockTts.currentlySpeaking());
+      assertEquals(this.mockTts.pendingUtterances().length, 1);
+      this.assertEqualsCollapseWhitespace(
+          this.mockTts.pendingUtterances()[0], 'Paragraph two.');
 
-            // Hitting pause and resume will start reading the remaining content
-            // in the second paragraph.
-            selectToSpeak.onPauseRequested();
-            assertFalse(this.mockTts.currentlySpeaking());
-            assertEquals(this.mockTts.pendingUtterances().length, 0);
-            selectToSpeak.onResumeRequested();
-            assertTrue(this.mockTts.currentlySpeaking());
-            assertEquals(this.mockTts.pendingUtterances().length, 1);
-            this.assertEqualsCollapseWhitespace(
-                this.mockTts.pendingUtterances()[0], 'two.');
+      // Hitting pause and resume will start reading the remaining content
+      // in the second paragraph.
+      selectToSpeak.onPauseRequested();
+      assertFalse(this.mockTts.currentlySpeaking());
+      assertEquals(this.mockTts.pendingUtterances().length, 0);
+      selectToSpeak.onResumeRequested();
+      assertTrue(this.mockTts.currentlySpeaking());
+      assertEquals(this.mockTts.pendingUtterances().length, 1);
+      this.assertEqualsCollapseWhitespace(
+          this.mockTts.pendingUtterances()[0], 'two.');
 
-            // Should not keep reading beyond the second paragraph.
-            this.mockTts.finishPendingUtterance();
-            assertFalse(this.mockTts.currentlySpeaking());
-            assertEquals(this.mockTts.pendingUtterances().length, 0);
-          });
+      // Should not keep reading beyond the second paragraph.
+      this.mockTts.finishPendingUtterance();
+      assertFalse(this.mockTts.currentlySpeaking());
+      assertEquals(this.mockTts.pendingUtterances().length, 0);
     });
 
 TEST_F(
     'SelectToSpeakNavigationControlTest', 'PauseResumeAfterSentenceNavigation',
-    function() {
+    async function() {
       const bodyHtml = `
       <span id='s1'>
         <p>Sentence one. Sentence two.</p>
         <p>Paragraph two.</p>
       </span>'
       `;
-      this.runWithLoadedTree(
-          this.generateHtmlWithSelectedElement('s1', bodyHtml),
-          async function() {
-            this.triggerReadSelectedText();
-            // Navigates to the next sentence and speaks until the last word
-            // (i.e., "two") in the first pargraph.
-            await selectToSpeak.onNextSentenceRequested();
-            this.mockTts.speakUntilCharIndex(23);
-            assertTrue(this.mockTts.currentlySpeaking());
-            assertEquals(this.mockTts.pendingUtterances().length, 1);
-            this.assertEqualsCollapseWhitespace(
-                this.mockTts.pendingUtterances()[0], 'Sentence two.');
+      await this.runWithLoadedTree(
+          this.generateHtmlWithSelectedElement('s1', bodyHtml));
+      this.triggerReadSelectedText();
+      // Navigates to the next sentence and speaks until the last word
+      // (i.e., "two") in the first pargraph.
+      await selectToSpeak.onNextSentenceRequested();
+      this.mockTts.speakUntilCharIndex(23);
+      assertTrue(this.mockTts.currentlySpeaking());
+      assertEquals(this.mockTts.pendingUtterances().length, 1);
+      this.assertEqualsCollapseWhitespace(
+          this.mockTts.pendingUtterances()[0], 'Sentence two.');
 
-            // Hitting pause and resume will start reading the remaining content
-            // in the first paragraph.
-            selectToSpeak.onPauseRequested();
-            assertFalse(this.mockTts.currentlySpeaking());
-            assertEquals(this.mockTts.pendingUtterances().length, 0);
-            selectToSpeak.onResumeRequested();
-            assertTrue(this.mockTts.currentlySpeaking());
-            assertEquals(this.mockTts.pendingUtterances().length, 1);
-            this.assertEqualsCollapseWhitespace(
-                this.mockTts.pendingUtterances()[0], 'two.');
+      // Hitting pause and resume will start reading the remaining content
+      // in the first paragraph.
+      selectToSpeak.onPauseRequested();
+      assertFalse(this.mockTts.currentlySpeaking());
+      assertEquals(this.mockTts.pendingUtterances().length, 0);
+      selectToSpeak.onResumeRequested();
+      assertTrue(this.mockTts.currentlySpeaking());
+      assertEquals(this.mockTts.pendingUtterances().length, 1);
+      this.assertEqualsCollapseWhitespace(
+          this.mockTts.pendingUtterances()[0], 'two.');
 
-            // Should not keep reading beyond the first paragraph.
-            this.mockTts.finishPendingUtterance();
-            assertFalse(this.mockTts.currentlySpeaking());
-            assertEquals(this.mockTts.pendingUtterances().length, 0);
-          });
+      // Should not keep reading beyond the first paragraph.
+      this.mockTts.finishPendingUtterance();
+      assertFalse(this.mockTts.currentlySpeaking());
+      assertEquals(this.mockTts.pendingUtterances().length, 0);
     });
 
 TEST_F(
     'SelectToSpeakNavigationControlTest', 'PauseResumeAtTheEndOfNodeGroupItem',
-    function() {
+    async function() {
       const bodyHtml = `
         <p id="p1">Sentence <span>one</span>. Sentence two.</p>
       `;
-      this.runWithLoadedTree(
-          this.generateHtmlWithSelectedElement('p1', bodyHtml), () => {
-            this.triggerReadSelectedText();
+      await this.runWithLoadedTree(
+          this.generateHtmlWithSelectedElement('p1', bodyHtml));
+      this.triggerReadSelectedText();
 
-            // Finishes the second word.
-            this.mockTts.speakUntilCharIndex(13);
-            assertTrue(this.mockTts.currentlySpeaking());
-            assertEquals(this.mockTts.pendingUtterances().length, 1);
-            this.assertEqualsCollapseWhitespace(
-                this.mockTts.pendingUtterances()[0],
-                'Sentence one . Sentence two.');
+      // Finishes the second word.
+      this.mockTts.speakUntilCharIndex(13);
+      assertTrue(this.mockTts.currentlySpeaking());
+      assertEquals(this.mockTts.pendingUtterances().length, 1);
+      this.assertEqualsCollapseWhitespace(
+          this.mockTts.pendingUtterances()[0], 'Sentence one . Sentence two.');
 
-            // Hitting pause will stop the current TTS.
-            selectToSpeak.onPauseRequested();
-            assertFalse(this.mockTts.currentlySpeaking());
-            assertEquals(this.mockTts.pendingUtterances().length, 0);
+      // Hitting pause will stop the current TTS.
+      selectToSpeak.onPauseRequested();
+      assertFalse(this.mockTts.currentlySpeaking());
+      assertEquals(this.mockTts.pendingUtterances().length, 0);
 
-            // Hitting resume will start from the remaining content of the
-            // paragraph.
-            selectToSpeak.onResumeRequested();
-            assertTrue(this.mockTts.currentlySpeaking());
-            assertEquals(this.mockTts.pendingUtterances().length, 1);
-            this.assertEqualsCollapseWhitespace(
-                this.mockTts.pendingUtterances()[0], '. Sentence two.');
-          });
+      // Hitting resume will start from the remaining content of the
+      // paragraph.
+      selectToSpeak.onResumeRequested();
+      assertTrue(this.mockTts.currentlySpeaking());
+      assertEquals(this.mockTts.pendingUtterances().length, 1);
+      this.assertEqualsCollapseWhitespace(
+          this.mockTts.pendingUtterances()[0], '. Sentence two.');
     });
 
 TEST_F(
     'SelectToSpeakNavigationControlTest', 'PauseResumeFromKeystrokeSelection',
-    function() {
+    async function() {
       const bodyHtml =
           '<p>This is some <b>bold</b> text</p><p>Second paragraph</p>';
       const setFocusCallback = this.newCallback((root) => {
@@ -457,387 +446,374 @@
           focusOffset: 6
         });
       });
-      this.runWithLoadedTree(bodyHtml, function(root) {
-        root.addEventListener(
-            'documentSelectionChanged', this.newCallback(function(event) {
-              this.triggerReadSelectedText();
-
-              // Speaks the first word 'is', the char index will count from the
-              // beginning of the node (i.e., from "This").
-              this.mockTts.speakUntilCharIndex(8);
-              assertTrue(this.mockTts.currentlySpeaking());
-              assertEquals(this.mockTts.pendingUtterances().length, 1);
-              this.assertEqualsCollapseWhitespace(
-                  this.mockTts.pendingUtterances()[0], 'is some bold text');
-
-              // Hitting pause will stop the current TTS.
-              selectToSpeak.onPauseRequested();
-              assertFalse(this.mockTts.currentlySpeaking());
-              assertEquals(this.mockTts.pendingUtterances().length, 0);
-
-              // Hitting resume will start from the remaining content of the
-              // paragraph.
-              selectToSpeak.onResumeRequested();
-              assertTrue(this.mockTts.currentlySpeaking());
-              assertEquals(this.mockTts.pendingUtterances().length, 1);
-              this.assertEqualsCollapseWhitespace(
-                  this.mockTts.pendingUtterances()[0], 'some bold text');
-
-              // Keep reading will finish all the content.
-              this.mockTts.finishPendingUtterance();
-              assertTrue(this.mockTts.currentlySpeaking());
-              assertEquals(this.mockTts.pendingUtterances().length, 1);
-              this.assertEqualsCollapseWhitespace(
-                  this.mockTts.pendingUtterances()[0], 'Second');
-            }),
-            false);
-        setFocusCallback(root);
-      });
-    });
-
-TEST_F('SelectToSpeakNavigationControlTest', 'NextSentence', function() {
-  const bodyHtml = `
-      <p id="p1">This is the first. This is the second.</p>'
-    `;
-  this.runWithLoadedTree(
-      this.generateHtmlWithSelectedElement('p1', bodyHtml), async function() {
-        this.triggerReadSelectedText();
-
-        // Speaks the first word.
-        this.mockTts.speakUntilCharIndex(5);
-        assertTrue(this.mockTts.currentlySpeaking());
-        assertEquals(this.mockTts.pendingUtterances().length, 1);
-        this.assertEqualsCollapseWhitespace(
-            this.mockTts.pendingUtterances()[0],
-            'This is the first. This is the second.');
-
-        // Hitting next sentence will start another TTS.
-        await selectToSpeak.onNextSentenceRequested();
-        assertTrue(this.mockTts.currentlySpeaking());
-        assertEquals(this.mockTts.pendingUtterances().length, 1);
-        this.assertEqualsCollapseWhitespace(
-            this.mockTts.pendingUtterances()[0], 'This is the second.');
-      });
-});
-
-TEST_F(
-    'SelectToSpeakNavigationControlTest', 'NextSentenceWithinParagraph',
-    function() {
-      const bodyHtml = `
-        <p id="p1">Sent 1. <span id="s1">Sent 2.</span> Sent 3. Sent 4.</p>
-      `;
-      this.runWithLoadedTree(
-          this.generateHtmlWithSelectedElement('s1', bodyHtml), () => {
+      const root = await this.runWithLoadedTree(bodyHtml);
+      root.addEventListener(
+          'documentSelectionChanged', this.newCallback(function(event) {
             this.triggerReadSelectedText();
 
-            // Speaks the first word.
-            this.mockTts.speakUntilCharIndex(5);
+            // Speaks the first word 'is', the char index will count from the
+            // beginning of the node (i.e., from "This").
+            this.mockTts.speakUntilCharIndex(8);
             assertTrue(this.mockTts.currentlySpeaking());
             assertEquals(this.mockTts.pendingUtterances().length, 1);
             this.assertEqualsCollapseWhitespace(
-                this.mockTts.pendingUtterances()[0], 'Sent 2.');
+                this.mockTts.pendingUtterances()[0], 'is some bold text');
 
-            // Hitting next sentence will start from the next sentence.
-            selectToSpeak.onNextSentenceRequested();
-            this.waitOneEventLoop(() => {
-              assertTrue(this.mockTts.currentlySpeaking());
-              assertEquals(this.mockTts.pendingUtterances().length, 1);
-              this.assertEqualsCollapseWhitespace(
-                  this.mockTts.pendingUtterances()[0], 'Sent 3. Sent 4.');
-            });
-          });
-    });
-
-TEST_F(
-    'SelectToSpeakNavigationControlTest', 'NextSentenceAcrossParagraph',
-    function() {
-      const bodyHtml = `
-        <p id="p1">Sent 1.</p>
-        <p id="p2">Sent 2. Sent 3.</p>'
-      `;
-      this.runWithLoadedTree(
-          this.generateHtmlWithSelectedElement('p1', bodyHtml), () => {
-            this.triggerReadSelectedText();
-
-            // Speaks the first word.
-            this.mockTts.speakUntilCharIndex(5);
-            assertTrue(this.mockTts.currentlySpeaking());
-            assertEquals(this.mockTts.pendingUtterances().length, 1);
-            this.assertEqualsCollapseWhitespace(
-                this.mockTts.pendingUtterances()[0], 'Sent 1.');
-
-            // Hitting next sentence will star from the next paragraph as there
-            // is no more sentence in the current paragraph.
-            selectToSpeak.onNextSentenceRequested();
-            this.waitOneEventLoop(() => {
-              assertTrue(this.mockTts.currentlySpeaking());
-              assertEquals(this.mockTts.pendingUtterances().length, 1);
-              this.assertEqualsCollapseWhitespace(
-                  this.mockTts.pendingUtterances()[0], 'Sent 2. Sent 3.');
-            });
-          });
-    });
-
-TEST_F('SelectToSpeakNavigationControlTest', 'PrevSentence', function() {
-  const bodyHtml = `
-      <p id="p1">First sentence. Second sentence. Third sentence.</p>'
-    `;
-  this.runWithLoadedTree(
-      this.generateHtmlWithSelectedElement('p1', bodyHtml), async function() {
-        this.triggerReadSelectedText();
-
-        // Speaks util the start of the second sentence.
-        this.mockTts.speakUntilCharIndex(33);
-        assertTrue(this.mockTts.currentlySpeaking());
-        assertEquals(this.mockTts.pendingUtterances().length, 1);
-        this.assertEqualsCollapseWhitespace(
-            this.mockTts.pendingUtterances()[0],
-            'First sentence. Second sentence. Third sentence.');
-
-        // Hitting prev sentence will start another TTS.
-        await selectToSpeak.onPreviousSentenceRequested();
-        assertTrue(this.mockTts.currentlySpeaking());
-        assertEquals(this.mockTts.pendingUtterances().length, 1);
-        this.assertEqualsCollapseWhitespace(
-            this.mockTts.pendingUtterances()[0],
-            'Second sentence. Third sentence.');
-      });
-});
-
-TEST_F(
-    'SelectToSpeakNavigationControlTest', 'PrevSentenceFromMiddleOfSentence',
-    function() {
-      const bodyHtml = `
-      <p id="p1">First sentence. Second sentence. Third sentence.</p>'
-    `;
-      this.runWithLoadedTree(
-          this.generateHtmlWithSelectedElement('p1', bodyHtml),
-          async function() {
-            this.triggerReadSelectedText();
-
-            // Speaks util the start of "sentence" in "Second sentence".
-            this.mockTts.speakUntilCharIndex(23);
-            assertTrue(this.mockTts.currentlySpeaking());
-            assertEquals(this.mockTts.pendingUtterances().length, 1);
-            this.assertEqualsCollapseWhitespace(
-                this.mockTts.pendingUtterances()[0],
-                'First sentence. Second sentence. Third sentence.');
-
-            // Hitting prev sentence will start another TTS.
-            await selectToSpeak.onPreviousSentenceRequested();
-            assertTrue(this.mockTts.currentlySpeaking());
-            assertEquals(this.mockTts.pendingUtterances().length, 1);
-            this.assertEqualsCollapseWhitespace(
-                this.mockTts.pendingUtterances()[0],
-                'First sentence. Second sentence. Third sentence.');
-          });
-    });
-
-TEST_F(
-    'SelectToSpeakNavigationControlTest', 'PrevSentenceWithinParagraph',
-    function() {
-      const bodyHtml = `
-      <p id="p1">Sent 0. Sent 1. <span id="s1">Sent 2.</span> Sent 3.</p>
-    `;
-      this.runWithLoadedTree(
-          this.generateHtmlWithSelectedElement('s1', bodyHtml), () => {
-            this.triggerReadSelectedText();
-
-            // Supposing we are at the start of the sentence.
-            assertTrue(this.mockTts.currentlySpeaking());
-            assertEquals(this.mockTts.pendingUtterances().length, 1);
-            this.assertEqualsCollapseWhitespace(
-                this.mockTts.pendingUtterances()[0], 'Sent 2.');
-
-            // Hitting previous sentence will start from the previous sentence.
-            selectToSpeak.onPreviousSentenceRequested();
-            this.waitOneEventLoop(() => {
-              assertTrue(this.mockTts.currentlySpeaking());
-              assertEquals(this.mockTts.pendingUtterances().length, 1);
-              this.assertEqualsCollapseWhitespace(
-                  this.mockTts.pendingUtterances()[0],
-                  'Sent 1. Sent 2. Sent 3.');
-            });
-          });
-    });
-
-TEST_F(
-    'SelectToSpeakNavigationControlTest', 'PrevSentenceAcrossParagraph',
-    function() {
-      const bodyHtml = `
-      <p id="p1">Sent 1. Sent 2.</p>
-      <p id="p2">Sent 3.</p>'
-    `;
-      this.runWithLoadedTree(
-          this.generateHtmlWithSelectedElement('p2', bodyHtml), () => {
-            this.triggerReadSelectedText();
-
-            // We are at the start of the sentence.
-            assertTrue(this.mockTts.currentlySpeaking());
-            assertEquals(this.mockTts.pendingUtterances().length, 1);
-            this.assertEqualsCollapseWhitespace(
-                this.mockTts.pendingUtterances()[0], 'Sent 3.');
-
-            // Hitting previous sentence will start from the last sentence in
-            // the previous paragraph as there is no more sentence in the
-            // current paragraph.
-            selectToSpeak.onPreviousSentenceRequested();
-            this.waitOneEventLoop(() => {
-              assertTrue(this.mockTts.currentlySpeaking());
-              assertEquals(this.mockTts.pendingUtterances().length, 1);
-              this.assertEqualsCollapseWhitespace(
-                  this.mockTts.pendingUtterances()[0], 'Sent 2.');
-            });
-          });
-    });
-
-TEST_F(
-    'SelectToSpeakNavigationControlTest', 'ChangeSpeedWhilePlaying',
-    function() {
-      chrome.settingsPrivate.setPref('settings.tts.speech_rate', 1.2);
-      const bodyHtml = `
-      <p id="p1">Paragraph 1</p>'
-    `;
-      this.runWithLoadedTree(
-          this.generateHtmlWithSelectedElement('p1', bodyHtml), () => {
-            this.triggerReadSelectedText();
-
-            // Speaks the first word.
-            this.mockTts.speakUntilCharIndex(10);
-            assertTrue(this.mockTts.currentlySpeaking());
-            assertEquals(this.mockTts.pendingUtterances().length, 1);
-            this.assertEqualsCollapseWhitespace(
-                this.mockTts.pendingUtterances()[0], 'Paragraph 1');
-            assertEquals(this.mockTts.getOptions().rate, 1.2);
-
-            // Changing speed will resume with the remaining content of the
-            // current sentence.
-            selectToSpeak.onChangeSpeedRequested(1.5);
-            assertFalse(this.mockTts.currentlySpeaking());
-            assertEquals(this.mockTts.pendingUtterances().length, 0);
-
-            // Wait an event loop so all pending promises are resolved prior to
-            // asserting that TTS resumed with the proper rate.
-            setTimeout(
-                this.newCallback(() => {
-                  // Should resume TTS with the remaining content with adjusted
-                  // rate.
-                  assertTrue(this.mockTts.currentlySpeaking());
-                  assertEquals(this.mockTts.getOptions().rate, 1.8);
-                  assertEquals(this.mockTts.pendingUtterances().length, 1);
-                  this.assertEqualsCollapseWhitespace(
-                      this.mockTts.pendingUtterances()[0], '1');
-                }),
-                0);
-          });
-    });
-
-TEST_F('SelectToSpeakNavigationControlTest', 'RetainsSpeedChange', function() {
-  chrome.settingsPrivate.setPref('settings.tts.speech_rate', 1.0);
-  const bodyHtml = `
-    <p id="p1">Paragraph 1</p>'
-  `;
-  this.runWithLoadedTree(
-      this.generateHtmlWithSelectedElement('p1', bodyHtml), () => {
-        this.triggerReadSelectedText();
-
-        // Changing speed then exit.
-        selectToSpeak.onChangeSpeedRequested(1.5);
-        selectToSpeak.onExitRequested();
-        assertFalse(this.mockTts.currentlySpeaking());
-        assertEquals(this.mockTts.pendingUtterances().length, 0);
-
-        // Next TTS session should remember previous rate.
-        this.triggerReadSelectedText();
-        assertTrue(this.mockTts.currentlySpeaking());
-        assertEquals(this.mockTts.getOptions().rate, 1.5);
-      });
-});
-
-TEST_F(
-    'SelectToSpeakNavigationControlTest', 'ChangeSpeedWhilePaused', function() {
-      chrome.settingsPrivate.setPref('settings.tts.speech_rate', 1.2);
-      const bodyHtml = `
-      <p id="p1">Paragraph 1</p>'
-    `;
-      this.runWithLoadedTree(
-          this.generateHtmlWithSelectedElement('p1', bodyHtml), () => {
-            this.triggerReadSelectedText();
-
-            // Speaks the first word.
-            this.mockTts.speakUntilCharIndex(10);
-            assertTrue(this.mockTts.currentlySpeaking());
-            assertEquals(this.mockTts.pendingUtterances().length, 1);
-            this.assertEqualsCollapseWhitespace(
-                this.mockTts.pendingUtterances()[0], 'Paragraph 1');
-            assertEquals(this.mockTts.getOptions().rate, 1.2);
-
-            // User-intiated pause.
+            // Hitting pause will stop the current TTS.
             selectToSpeak.onPauseRequested();
             assertFalse(this.mockTts.currentlySpeaking());
             assertEquals(this.mockTts.pendingUtterances().length, 0);
 
-            // Changing speed will remain paused.
-            selectToSpeak.onChangeSpeedRequested(1.5);
+            // Hitting resume will start from the remaining content of the
+            // paragraph.
+            selectToSpeak.onResumeRequested();
+            assertTrue(this.mockTts.currentlySpeaking());
+            assertEquals(this.mockTts.pendingUtterances().length, 1);
+            this.assertEqualsCollapseWhitespace(
+                this.mockTts.pendingUtterances()[0], 'some bold text');
 
-            // Wait an event loop so all pending promises are resolved prior to
-            // asserting that TTS remains paused.
-            setTimeout(this.newCallback(() => {
-              assertFalse(this.mockTts.currentlySpeaking());
-              assertEquals(this.mockTts.pendingUtterances().length, 0);
-            }, 0));
-          });
+            // Keep reading will finish all the content.
+            this.mockTts.finishPendingUtterance();
+            assertTrue(this.mockTts.currentlySpeaking());
+            assertEquals(this.mockTts.pendingUtterances().length, 1);
+            this.assertEqualsCollapseWhitespace(
+                this.mockTts.pendingUtterances()[0], 'Second');
+          }),
+          false);
+      setFocusCallback(root);
+    });
+
+TEST_F('SelectToSpeakNavigationControlTest', 'NextSentence', async function() {
+  const bodyHtml = `
+      <p id="p1">This is the first. This is the second.</p>'
+    `;
+  await this.runWithLoadedTree(
+      this.generateHtmlWithSelectedElement('p1', bodyHtml));
+  this.triggerReadSelectedText();
+
+  // Speaks the first word.
+  this.mockTts.speakUntilCharIndex(5);
+  assertTrue(this.mockTts.currentlySpeaking());
+  assertEquals(this.mockTts.pendingUtterances().length, 1);
+  this.assertEqualsCollapseWhitespace(
+      this.mockTts.pendingUtterances()[0],
+      'This is the first. This is the second.');
+
+  // Hitting next sentence will start another TTS.
+  await selectToSpeak.onNextSentenceRequested();
+  assertTrue(this.mockTts.currentlySpeaking());
+  assertEquals(this.mockTts.pendingUtterances().length, 1);
+  this.assertEqualsCollapseWhitespace(
+      this.mockTts.pendingUtterances()[0], 'This is the second.');
+});
+
+TEST_F(
+    'SelectToSpeakNavigationControlTest', 'NextSentenceWithinParagraph',
+    async function() {
+      const bodyHtml = `
+        <p id="p1">Sent 1. <span id="s1">Sent 2.</span> Sent 3. Sent 4.</p>
+      `;
+      await this.runWithLoadedTree(
+          this.generateHtmlWithSelectedElement('s1', bodyHtml));
+      this.triggerReadSelectedText();
+
+      // Speaks the first word.
+      this.mockTts.speakUntilCharIndex(5);
+      assertTrue(this.mockTts.currentlySpeaking());
+      assertEquals(this.mockTts.pendingUtterances().length, 1);
+      this.assertEqualsCollapseWhitespace(
+          this.mockTts.pendingUtterances()[0], 'Sent 2.');
+
+      // Hitting next sentence will start from the next sentence.
+      selectToSpeak.onNextSentenceRequested();
+      this.waitOneEventLoop(() => {
+        assertTrue(this.mockTts.currentlySpeaking());
+        assertEquals(this.mockTts.pendingUtterances().length, 1);
+        this.assertEqualsCollapseWhitespace(
+            this.mockTts.pendingUtterances()[0], 'Sent 3. Sent 4.');
+      });
+    });
+
+TEST_F(
+    'SelectToSpeakNavigationControlTest', 'NextSentenceAcrossParagraph',
+    async function() {
+      const bodyHtml = `
+        <p id="p1">Sent 1.</p>
+        <p id="p2">Sent 2. Sent 3.</p>'
+      `;
+      await this.runWithLoadedTree(
+          this.generateHtmlWithSelectedElement('p1', bodyHtml));
+      this.triggerReadSelectedText();
+
+      // Speaks the first word.
+      this.mockTts.speakUntilCharIndex(5);
+      assertTrue(this.mockTts.currentlySpeaking());
+      assertEquals(this.mockTts.pendingUtterances().length, 1);
+      this.assertEqualsCollapseWhitespace(
+          this.mockTts.pendingUtterances()[0], 'Sent 1.');
+
+      // Hitting next sentence will star from the next paragraph as there
+      // is no more sentence in the current paragraph.
+      selectToSpeak.onNextSentenceRequested();
+      this.waitOneEventLoop(() => {
+        assertTrue(this.mockTts.currentlySpeaking());
+        assertEquals(this.mockTts.pendingUtterances().length, 1);
+        this.assertEqualsCollapseWhitespace(
+            this.mockTts.pendingUtterances()[0], 'Sent 2. Sent 3.');
+      });
+    });
+
+TEST_F('SelectToSpeakNavigationControlTest', 'PrevSentence', async function() {
+  const bodyHtml = `
+      <p id="p1">First sentence. Second sentence. Third sentence.</p>'
+    `;
+  await this.runWithLoadedTree(
+      this.generateHtmlWithSelectedElement('p1', bodyHtml));
+  this.triggerReadSelectedText();
+
+  // Speaks util the start of the second sentence.
+  this.mockTts.speakUntilCharIndex(33);
+  assertTrue(this.mockTts.currentlySpeaking());
+  assertEquals(this.mockTts.pendingUtterances().length, 1);
+  this.assertEqualsCollapseWhitespace(
+      this.mockTts.pendingUtterances()[0],
+      'First sentence. Second sentence. Third sentence.');
+
+  // Hitting prev sentence will start another TTS.
+  await selectToSpeak.onPreviousSentenceRequested();
+  assertTrue(this.mockTts.currentlySpeaking());
+  assertEquals(this.mockTts.pendingUtterances().length, 1);
+  this.assertEqualsCollapseWhitespace(
+      this.mockTts.pendingUtterances()[0], 'Second sentence. Third sentence.');
+});
+
+TEST_F(
+    'SelectToSpeakNavigationControlTest', 'PrevSentenceFromMiddleOfSentence',
+    async function() {
+      const bodyHtml = `
+      <p id="p1">First sentence. Second sentence. Third sentence.</p>'
+    `;
+      await this.runWithLoadedTree(
+          this.generateHtmlWithSelectedElement('p1', bodyHtml));
+      this.triggerReadSelectedText();
+
+      // Speaks util the start of "sentence" in "Second sentence".
+      this.mockTts.speakUntilCharIndex(23);
+      assertTrue(this.mockTts.currentlySpeaking());
+      assertEquals(this.mockTts.pendingUtterances().length, 1);
+      this.assertEqualsCollapseWhitespace(
+          this.mockTts.pendingUtterances()[0],
+          'First sentence. Second sentence. Third sentence.');
+
+      // Hitting prev sentence will start another TTS.
+      await selectToSpeak.onPreviousSentenceRequested();
+      assertTrue(this.mockTts.currentlySpeaking());
+      assertEquals(this.mockTts.pendingUtterances().length, 1);
+      this.assertEqualsCollapseWhitespace(
+          this.mockTts.pendingUtterances()[0],
+          'First sentence. Second sentence. Third sentence.');
+    });
+
+TEST_F(
+    'SelectToSpeakNavigationControlTest', 'PrevSentenceWithinParagraph',
+    async function() {
+      const bodyHtml = `
+      <p id="p1">Sent 0. Sent 1. <span id="s1">Sent 2.</span> Sent 3.</p>
+    `;
+      await this.runWithLoadedTree(
+          this.generateHtmlWithSelectedElement('s1', bodyHtml));
+      this.triggerReadSelectedText();
+
+      // Supposing we are at the start of the sentence.
+      assertTrue(this.mockTts.currentlySpeaking());
+      assertEquals(this.mockTts.pendingUtterances().length, 1);
+      this.assertEqualsCollapseWhitespace(
+          this.mockTts.pendingUtterances()[0], 'Sent 2.');
+
+      // Hitting previous sentence will start from the previous sentence.
+      selectToSpeak.onPreviousSentenceRequested();
+      this.waitOneEventLoop(() => {
+        assertTrue(this.mockTts.currentlySpeaking());
+        assertEquals(this.mockTts.pendingUtterances().length, 1);
+        this.assertEqualsCollapseWhitespace(
+            this.mockTts.pendingUtterances()[0], 'Sent 1. Sent 2. Sent 3.');
+      });
+    });
+
+TEST_F(
+    'SelectToSpeakNavigationControlTest', 'PrevSentenceAcrossParagraph',
+    async function() {
+      const bodyHtml = `
+      <p id="p1">Sent 1. Sent 2.</p>
+      <p id="p2">Sent 3.</p>'
+    `;
+      await this.runWithLoadedTree(
+          this.generateHtmlWithSelectedElement('p2', bodyHtml));
+      this.triggerReadSelectedText();
+
+      // We are at the start of the sentence.
+      assertTrue(this.mockTts.currentlySpeaking());
+      assertEquals(this.mockTts.pendingUtterances().length, 1);
+      this.assertEqualsCollapseWhitespace(
+          this.mockTts.pendingUtterances()[0], 'Sent 3.');
+
+      // Hitting previous sentence will start from the last sentence in
+      // the previous paragraph as there is no more sentence in the
+      // current paragraph.
+      selectToSpeak.onPreviousSentenceRequested();
+      this.waitOneEventLoop(() => {
+        assertTrue(this.mockTts.currentlySpeaking());
+        assertEquals(this.mockTts.pendingUtterances().length, 1);
+        this.assertEqualsCollapseWhitespace(
+            this.mockTts.pendingUtterances()[0], 'Sent 2.');
+      });
+    });
+
+TEST_F(
+    'SelectToSpeakNavigationControlTest', 'ChangeSpeedWhilePlaying',
+    async function() {
+      chrome.settingsPrivate.setPref('settings.tts.speech_rate', 1.2);
+      const bodyHtml = `
+      <p id="p1">Paragraph 1</p>'
+    `;
+      await this.runWithLoadedTree(
+          this.generateHtmlWithSelectedElement('p1', bodyHtml));
+      this.triggerReadSelectedText();
+
+      // Speaks the first word.
+      this.mockTts.speakUntilCharIndex(10);
+      assertTrue(this.mockTts.currentlySpeaking());
+      assertEquals(this.mockTts.pendingUtterances().length, 1);
+      this.assertEqualsCollapseWhitespace(
+          this.mockTts.pendingUtterances()[0], 'Paragraph 1');
+      assertEquals(this.mockTts.getOptions().rate, 1.2);
+
+      // Changing speed will resume with the remaining content of the
+      // current sentence.
+      selectToSpeak.onChangeSpeedRequested(1.5);
+      assertFalse(this.mockTts.currentlySpeaking());
+      assertEquals(this.mockTts.pendingUtterances().length, 0);
+
+      // Wait an event loop so all pending promises are resolved prior to
+      // asserting that TTS resumed with the proper rate.
+      setTimeout(
+          this.newCallback(() => {
+            // Should resume TTS with the remaining content with adjusted
+            // rate.
+            assertTrue(this.mockTts.currentlySpeaking());
+            assertEquals(this.mockTts.getOptions().rate, 1.8);
+            assertEquals(this.mockTts.pendingUtterances().length, 1);
+            this.assertEqualsCollapseWhitespace(
+                this.mockTts.pendingUtterances()[0], '1');
+          }),
+          0);
+    });
+
+TEST_F(
+    'SelectToSpeakNavigationControlTest', 'RetainsSpeedChange',
+    async function() {
+      chrome.settingsPrivate.setPref('settings.tts.speech_rate', 1.0);
+      const bodyHtml = `
+    <p id="p1">Paragraph 1</p>'
+  `;
+      await this.runWithLoadedTree(
+          this.generateHtmlWithSelectedElement('p1', bodyHtml));
+      this.triggerReadSelectedText();
+
+      // Changing speed then exit.
+      selectToSpeak.onChangeSpeedRequested(1.5);
+      selectToSpeak.onExitRequested();
+      assertFalse(this.mockTts.currentlySpeaking());
+      assertEquals(this.mockTts.pendingUtterances().length, 0);
+
+      // Next TTS session should remember previous rate.
+      this.triggerReadSelectedText();
+      assertTrue(this.mockTts.currentlySpeaking());
+      assertEquals(this.mockTts.getOptions().rate, 1.5);
+    });
+
+TEST_F(
+    'SelectToSpeakNavigationControlTest', 'ChangeSpeedWhilePaused',
+    async function() {
+      chrome.settingsPrivate.setPref('settings.tts.speech_rate', 1.2);
+      const bodyHtml = `
+      <p id="p1">Paragraph 1</p>'
+    `;
+      await this.runWithLoadedTree(
+          this.generateHtmlWithSelectedElement('p1', bodyHtml));
+      this.triggerReadSelectedText();
+
+      // Speaks the first word.
+      this.mockTts.speakUntilCharIndex(10);
+      assertTrue(this.mockTts.currentlySpeaking());
+      assertEquals(this.mockTts.pendingUtterances().length, 1);
+      this.assertEqualsCollapseWhitespace(
+          this.mockTts.pendingUtterances()[0], 'Paragraph 1');
+      assertEquals(this.mockTts.getOptions().rate, 1.2);
+
+      // User-intiated pause.
+      selectToSpeak.onPauseRequested();
+      assertFalse(this.mockTts.currentlySpeaking());
+      assertEquals(this.mockTts.pendingUtterances().length, 0);
+
+      // Changing speed will remain paused.
+      selectToSpeak.onChangeSpeedRequested(1.5);
+
+      // Wait an event loop so all pending promises are resolved prior to
+      // asserting that TTS remains paused.
+      setTimeout(this.newCallback(() => {
+        assertFalse(this.mockTts.currentlySpeaking());
+        assertEquals(this.mockTts.pendingUtterances().length, 0);
+      }, 0));
     });
 
 TEST_F(
     'SelectToSpeakNavigationControlTest', 'ResumeAtTheEndOfParagraph',
-    function() {
+    async function() {
       const bodyHtml = `
         <p id="p1">Paragraph 1</p>
         <p id="p2">Paragraph 2</p>
       `;
-      this.runWithLoadedTree(
-          this.generateHtmlWithSelectedElement('p1', bodyHtml), () => {
-            this.triggerReadSelectedText();
+      await this.runWithLoadedTree(
+          this.generateHtmlWithSelectedElement('p1', bodyHtml));
+      this.triggerReadSelectedText();
 
-            // Finishes the current utterance.
-            this.mockTts.finishPendingUtterance();
+      // Finishes the current utterance.
+      this.mockTts.finishPendingUtterance();
 
-            // Hitting resume will start the next paragraph.
-            selectToSpeak.onResumeRequested();
-            assertTrue(this.mockTts.currentlySpeaking());
-            assertEquals(this.mockTts.pendingUtterances().length, 1);
-            this.assertEqualsCollapseWhitespace(
-                this.mockTts.pendingUtterances()[0], 'Paragraph 2');
-          });
+      // Hitting resume will start the next paragraph.
+      selectToSpeak.onResumeRequested();
+      assertTrue(this.mockTts.currentlySpeaking());
+      assertEquals(this.mockTts.pendingUtterances().length, 1);
+      this.assertEqualsCollapseWhitespace(
+          this.mockTts.pendingUtterances()[0], 'Paragraph 2');
     });
 
 TEST_F(
     'SelectToSpeakNavigationControlTest', 'ResumeAtTheEndOfUserSelection',
-    function() {
+    async function() {
       const bodyHtml = `
         <p id="p1">Sentence <span id="s1">one</span>. Sentence two.</p>
         <p id="p2">Paragraph 2</p>
       `;
-      this.runWithLoadedTree(
-          this.generateHtmlWithSelectedElement('s1', bodyHtml), () => {
-            this.triggerReadSelectedText();
+      await this.runWithLoadedTree(
+          this.generateHtmlWithSelectedElement('s1', bodyHtml));
+      this.triggerReadSelectedText();
 
-            // Finishes the current utterance.
-            this.mockTts.finishPendingUtterance();
+      // Finishes the current utterance.
+      this.mockTts.finishPendingUtterance();
 
-            // Hitting resume will start the remaining content.
-            selectToSpeak.onResumeRequested();
-            assertTrue(this.mockTts.currentlySpeaking());
-            assertEquals(this.mockTts.pendingUtterances().length, 1);
-            this.assertEqualsCollapseWhitespace(
-                this.mockTts.pendingUtterances()[0], '. Sentence two.');
-          });
+      // Hitting resume will start the remaining content.
+      selectToSpeak.onResumeRequested();
+      assertTrue(this.mockTts.currentlySpeaking());
+      assertEquals(this.mockTts.pendingUtterances().length, 1);
+      this.assertEqualsCollapseWhitespace(
+          this.mockTts.pendingUtterances()[0], '. Sentence two.');
     });
 
 TEST_F(
     'SelectToSpeakNavigationControlTest', 'ResumeFromSelectionEndingInSpace',
-    function() {
+    async function() {
       const bodyHtml = '<p>This is some text with space.</p>';
       const setFocusCallback = this.newCallback((root) => {
         const node = this.findTextNode(root, 'This is some text with space.');
@@ -849,37 +825,39 @@
           focusOffset: 5
         });
       });
-      this.runWithLoadedTree(bodyHtml, (root) => {
-        root.addEventListener(
-            'documentSelectionChanged', this.newCallback((event) => {
-              this.triggerReadSelectedText();
+      const root = await this.runWithLoadedTree(bodyHtml);
+      root.addEventListener(
+          'documentSelectionChanged', this.newCallback((event) => {
+            this.triggerReadSelectedText();
 
-              assertTrue(this.mockTts.currentlySpeaking());
-              assertEquals(this.mockTts.pendingUtterances().length, 1);
-              this.assertEqualsCollapseWhitespace(
-                  this.mockTts.pendingUtterances()[0], 'This');
+            assertTrue(this.mockTts.currentlySpeaking());
+            assertEquals(this.mockTts.pendingUtterances().length, 1);
+            this.assertEqualsCollapseWhitespace(
+                this.mockTts.pendingUtterances()[0], 'This');
 
-              // Finishes the current utterance.
-              this.mockTts.finishPendingUtterance();
+            // Finishes the current utterance.
+            this.mockTts.finishPendingUtterance();
 
-              // Hitting resume will start from the remaining content of the
-              // paragraph.
-              selectToSpeak.onResumeRequested();
-              assertTrue(this.mockTts.currentlySpeaking());
-              assertEquals(this.mockTts.pendingUtterances().length, 1);
-              this.assertEqualsCollapseWhitespace(
-                  this.mockTts.pendingUtterances()[0],
-                  'is some text with space.');
-            }),
-            false);
-        setFocusCallback(root);
-      });
+            // Hitting resume will start from the remaining content of the
+            // paragraph.
+            selectToSpeak.onResumeRequested();
+            assertTrue(this.mockTts.currentlySpeaking());
+            assertEquals(this.mockTts.pendingUtterances().length, 1);
+            this.assertEqualsCollapseWhitespace(
+                this.mockTts.pendingUtterances()[0],
+                'is some text with space.');
+          }),
+          false);
+      setFocusCallback(root);
     });
 
-TEST_F('SelectToSpeakNavigationControlTest', 'ResizeWhilePlaying', function() {
-  const longLine =
-      'Second paragraph is longer than 300 pixels and will wrap when resized';
-  const bodyHtml = `
+TEST_F(
+    'SelectToSpeakNavigationControlTest', 'ResizeWhilePlaying',
+    async function() {
+      const longLine =
+          'Second paragraph is longer than 300 pixels and will wrap when' +
+          'resized';
+      const bodyHtml = `
           <script type="text/javascript">
             function doResize() {
               document.getElementById('resize').style.width = '100px';
@@ -893,180 +871,172 @@
           </div>
           <button onclick="doResize()">Resize</button>
         `;
-  this.runWithLoadedTree(
-      this.generateHtmlWithSelectedElement('content', bodyHtml), (root) => {
-        this.triggerReadSelectedText();
+      const root = await this.runWithLoadedTree(
+          this.generateHtmlWithSelectedElement('content', bodyHtml));
+      this.triggerReadSelectedText();
 
-        // Speaks the first paragraph.
+      // Speaks the first paragraph.
+      assertTrue(this.mockTts.currentlySpeaking());
+      assertEquals(this.mockTts.pendingUtterances().length, 1);
+      this.assertEqualsCollapseWhitespace(
+          this.mockTts.pendingUtterances()[0], 'First paragraph');
+
+      const resizeButton =
+          root.find({role: 'button', attributes: {name: 'Resize'}});
+
+      // Wait for click event, at which point the automation tree should
+      // be updated from the resize.
+      resizeButton.addEventListener(EventType.CLICKED, this.newCallback(() => {
+        // Trigger next node group by completing first TTS request.
+        this.mockTts.finishPendingUtterance();
+
+        // Should still read second paragraph, even though some nodes
+        // were invalided from the resize.
         assertTrue(this.mockTts.currentlySpeaking());
         assertEquals(this.mockTts.pendingUtterances().length, 1);
         this.assertEqualsCollapseWhitespace(
-            this.mockTts.pendingUtterances()[0], 'First paragraph');
+            this.mockTts.pendingUtterances()[0], longLine);
+      }));
 
-        const resizeButton =
-            root.find({role: 'button', attributes: {name: 'Resize'}});
-
-        // Wait for click event, at which point the automation tree should
-        // be updated from the resize.
-        resizeButton.addEventListener(
-            EventType.CLICKED, this.newCallback(() => {
-              // Trigger next node group by completing first TTS request.
-              this.mockTts.finishPendingUtterance();
-
-              // Should still read second paragraph, even though some nodes
-              // were invalided from the resize.
-              assertTrue(this.mockTts.currentlySpeaking());
-              assertEquals(this.mockTts.pendingUtterances().length, 1);
-              this.assertEqualsCollapseWhitespace(
-                  this.mockTts.pendingUtterances()[0], longLine);
-            }));
-
-        // Perform resize.
-        resizeButton.doDefault();
-      });
-});
-
-TEST_F(
-    'SelectToSpeakNavigationControlTest',
-    'RemainsActiveAfterCompletingUtterance', function() {
-      const bodyHtml = '<p id="p1">Paragraph 1</p>';
-      this.runWithLoadedTree(
-          this.generateHtmlWithSelectedElement('p1', bodyHtml), () => {
-            // Simulate starting and completing TTS.
-            this.triggerReadSelectedText();
-            this.mockTts.finishPendingUtterance();
-
-            // Should remain in speaking state.
-            assertEquals(selectToSpeak.state_, SelectToSpeakState.SPEAKING);
-          });
+      // Perform resize.
+      resizeButton.doDefault();
     });
 
 TEST_F(
     'SelectToSpeakNavigationControlTest',
-    'AutoDismissesIfNavigationControlsDisabled', function() {
+    'RemainsActiveAfterCompletingUtterance', async function() {
+      const bodyHtml = '<p id="p1">Paragraph 1</p>';
+      await this.runWithLoadedTree(
+          this.generateHtmlWithSelectedElement('p1', bodyHtml));
+      // Simulate starting and completing TTS.
+      this.triggerReadSelectedText();
+      this.mockTts.finishPendingUtterance();
+
+      // Should remain in speaking state.
+      assertEquals(selectToSpeak.state_, SelectToSpeakState.SPEAKING);
+    });
+
+TEST_F(
+    'SelectToSpeakNavigationControlTest',
+    'AutoDismissesIfNavigationControlsDisabled', async function() {
       // Disable navigation controls via settings.
       chrome.storage.sync.set({'navigationControls': false});
       const bodyHtml = '<p id="p1">Paragraph 1</p>';
-      this.runWithLoadedTree(
-          this.generateHtmlWithSelectedElement('p1', bodyHtml), () => {
-            // Simulate starting and completing TTS.
-            this.triggerReadSelectedText();
-            this.mockTts.finishPendingUtterance();
+      await this.runWithLoadedTree(
+          this.generateHtmlWithSelectedElement('p1', bodyHtml));
+      // Simulate starting and completing TTS.
+      this.triggerReadSelectedText();
+      this.mockTts.finishPendingUtterance();
 
-            // Should auto-dismiss.
-            assertEquals(selectToSpeak.state_, SelectToSpeakState.INACTIVE);
-          });
+      // Should auto-dismiss.
+      assertEquals(selectToSpeak.state_, SelectToSpeakState.INACTIVE);
     });
 
 TEST_F(
     'SelectToSpeakNavigationControlTest', 'NavigatesToNextParagraphQuickly',
-    function() {
+    async function() {
       const bodyHtml = `
         <p id="p1">Paragraph 1</p>
         <p id="p2">Paragraph 2</p>'
       `;
-      this.runWithLoadedTree(
-          this.generateHtmlWithSelectedElement('p1', bodyHtml), () => {
-            // Have mock TTS engine wait to send events so we can simulate a
-            // delayed 'start' event.
-            this.mockTts.setWaitToSendEvents(true);
-            this.triggerReadSelectedText();
-            const speakOptions = this.mockTts.getOptions();
+      await this.runWithLoadedTree(
+          this.generateHtmlWithSelectedElement('p1', bodyHtml));
+      // Have mock TTS engine wait to send events so we can simulate a
+      // delayed 'start' event.
+      this.mockTts.setWaitToSendEvents(true);
+      this.triggerReadSelectedText();
+      const speakOptions = this.mockTts.getOptions();
 
-            // Navigate to next paragraph before speech begins.
-            selectToSpeak.onNextParagraphRequested();
+      // Navigate to next paragraph before speech begins.
+      selectToSpeak.onNextParagraphRequested();
 
-            this.waitOneEventLoop(() => {
-              // Manually triggered delayed events.
-              this.mockTts.sendPendingEvents();
+      this.waitOneEventLoop(() => {
+        // Manually triggered delayed events.
+        this.mockTts.sendPendingEvents();
 
-              // Should remain in speaking state.
-              assertEquals(selectToSpeak.state_, SelectToSpeakState.SPEAKING);
-            });
-          });
+        // Should remain in speaking state.
+        assertEquals(selectToSpeak.state_, SelectToSpeakState.SPEAKING);
+      });
     });
 
 TEST_F(
     'SelectToSpeakNavigationControlTest', 'SetsInitialFocusToPanel',
-    function() {
+    async function() {
       const bodyHtml = '<p id="p1">Sample text</p>';
-      this.runWithLoadedTree(
-          this.generateHtmlWithSelectedElement('p1', bodyHtml), (root) => {
-            const desktop = root.parent.root;
+      const root = await this.runWithLoadedTree(
+          this.generateHtmlWithSelectedElement('p1', bodyHtml));
+      const desktop = root.parent.root;
 
-            // Wait for button in STS panel to be focused.
-            // Test will fail if panel is never focused.
-            this.waitForPanelFocus(desktop, () => {});
+      // Wait for button in STS panel to be focused.
+      // Test will fail if panel is never focused.
+      this.waitForPanelFocus(desktop, () => {});
 
-            // Trigger STS, which will initially set focus to the panel.
-            this.triggerReadSelectedText();
-          });
+      // Trigger STS, which will initially set focus to the panel.
+      this.triggerReadSelectedText();
     });
 
 TEST_F(
     'SelectToSpeakNavigationControlTest', 'KeyboardShortcutKeepsFocusInPanel',
-    function() {
+    async function() {
       const bodyHtml = '<p id="p1">Sample text</p>';
-      this.runWithLoadedTree(
-          this.generateHtmlWithSelectedElement('p1', bodyHtml), (root) => {
-            const desktop = root.parent.root;
+      const root = await this.runWithLoadedTree(
+          this.generateHtmlWithSelectedElement('p1', bodyHtml));
+      const desktop = root.parent.root;
 
-            // Wait for button within STS panel is focused.
-            this.waitForPanelFocus(desktop, () => {
-              // Remove text selection.
-              const textNode = this.findTextNode(root, 'Sample text');
-              chrome.automation.setDocumentSelection({
-                anchorObject: textNode,
-                anchorOffset: 0,
-                focusObject: textNode,
-                focusOffset: 0
-              });
+      // Wait for button within STS panel is focused.
+      this.waitForPanelFocus(desktop, () => {
+        // Remove text selection.
+        const textNode = this.findTextNode(root, 'Sample text');
+        chrome.automation.setDocumentSelection({
+          anchorObject: textNode,
+          anchorOffset: 0,
+          focusObject: textNode,
+          focusOffset: 0
+        });
 
-              // Perform Search key + S, which should restore focus to
-              // panel.
-              selectToSpeak.fireMockKeyDownEvent(
-                  {keyCode: SelectToSpeakConstants.SEARCH_KEY_CODE});
-              selectToSpeak.fireMockKeyDownEvent(
-                  {keyCode: SelectToSpeakConstants.READ_SELECTION_KEY_CODE});
-              selectToSpeak.fireMockKeyUpEvent(
-                  {keyCode: SelectToSpeakConstants.READ_SELECTION_KEY_CODE});
-              selectToSpeak.fireMockKeyUpEvent(
-                  {keyCode: SelectToSpeakConstants.SEARCH_KEY_CODE});
+        // Perform Search key + S, which should restore focus to
+        // panel.
+        selectToSpeak.fireMockKeyDownEvent(
+            {keyCode: SelectToSpeakConstants.SEARCH_KEY_CODE});
+        selectToSpeak.fireMockKeyDownEvent(
+            {keyCode: SelectToSpeakConstants.READ_SELECTION_KEY_CODE});
+        selectToSpeak.fireMockKeyUpEvent(
+            {keyCode: SelectToSpeakConstants.READ_SELECTION_KEY_CODE});
+        selectToSpeak.fireMockKeyUpEvent(
+            {keyCode: SelectToSpeakConstants.SEARCH_KEY_CODE});
 
-              // Verify focus is still on button within panel.
-              chrome.automation.getFocus(this.newCallback((focusedNode) => {
-                assertEquals(focusedNode.role, RoleType.TOGGLE_BUTTON);
-                assertTrue(this.isNodeWithinPanel(focusedNode));
-              }));
-            });
+        // Verify focus is still on button within panel.
+        chrome.automation.getFocus(this.newCallback((focusedNode) => {
+          assertEquals(focusedNode.role, RoleType.TOGGLE_BUTTON);
+          assertTrue(this.isNodeWithinPanel(focusedNode));
+        }));
+      });
 
-            // Trigger STS, which will initially set focus to the panel.
-            this.triggerReadSelectedText();
-          });
+      // Trigger STS, which will initially set focus to the panel.
+      this.triggerReadSelectedText();
     });
 
 TEST_F(
     'SelectToSpeakNavigationControlTest', 'SelectingWindowDoesNotShowPanel',
-    function() {
+    async function() {
       const bodyHtml = `
         <title>Test</title>
         <div style="position: absolute; top: 300px;">
           Hello
         </div>
       `;
-      this.runWithLoadedTree(bodyHtml, (root) => {
-        // Expect call to updateSelectToSpeakPanel to set panel to be hidden.
-        chrome.accessibilityPrivate.updateSelectToSpeakPanel =
-            this.newCallback((visible) => {
-              assertFalse(visible);
-            });
+      const root = await this.runWithLoadedTree(bodyHtml);
+      // Expect call to updateSelectToSpeakPanel to set panel to be hidden.
+      chrome.accessibilityPrivate.updateSelectToSpeakPanel =
+          this.newCallback((visible) => {
+            assertFalse(visible);
+          });
 
-        // Trigger mouse selection on a part of the page where no text nodes
-        // exist, should select entire page.
-        const mouseEvent = {
-          screenX: root.location.left + 1,
-          screenY: root.location.top + 1,
-        };
-        this.triggerReadMouseSelectedText(mouseEvent, mouseEvent);
-      });
+      // Trigger mouse selection on a part of the page where no text nodes
+      // exist, should select entire page.
+      const mouseEvent = {
+        screenX: root.location.left + 1,
+        screenY: root.location.top + 1,
+      };
+      this.triggerReadMouseSelectedText(mouseEvent, mouseEvent);
     });
diff --git a/chrome/browser/resources/chromeos/accessibility/switch_access/auto_scan_manager_test.js b/chrome/browser/resources/chromeos/accessibility/switch_access/auto_scan_manager_test.js
index bf5b82531..1456f44 100644
--- a/chrome/browser/resources/chromeos/accessibility/switch_access/auto_scan_manager_test.js
+++ b/chrome/browser/resources/chromeos/accessibility/switch_access/auto_scan_manager_test.js
@@ -54,7 +54,7 @@
 };
 
 TEST_F('SwitchAccessAutoScanManagerTest', 'SetEnabled', function() {
-  this.runWithLoadedTree('', () => {
+  this.runWithLoadedDesktop(() => {
     assertFalse(
         AutoScanManager.instance.isRunning_(),
         'Auto scan manager is running prematurely');
diff --git a/chrome/browser/resources/chromeos/accessibility/switch_access/focus_ring_manager_test.js b/chrome/browser/resources/chromeos/accessibility/switch_access/focus_ring_manager_test.js
index d394a95..6e5eb18 100644
--- a/chrome/browser/resources/chromeos/accessibility/switch_access/focus_ring_manager_test.js
+++ b/chrome/browser/resources/chromeos/accessibility/switch_access/focus_ring_manager_test.js
@@ -51,87 +51,82 @@
 });
 
 TEST_F(
-    'SwitchAccessFocusRingManagerTest', 'BackButtonForMenuFocus', function() {
+    'SwitchAccessFocusRingManagerTest', 'BackButtonForMenuFocus',
+    async function() {
       const site = '<input type="text">';
-      this.runWithLoadedTree(site, async (rootWebArea) => {
-        // Open the menu and focus the back button.
-        TestUtility.startFocusInside(rootWebArea);
-        // Check the current node directly, as the TestUtility relies on the
-        // FocusManager to identify the current focus.
-        assertEquals(
-            chrome.automation.RoleType.TEXT_FIELD, Navigator.byItem.node_.role);
-        TestUtility.pressSelectSwitch();
+      const rootWebArea = await this.runWithLoadedTree(site);
+      // Open the menu and focus the back button.
+      TestUtility.startFocusInside(rootWebArea);
+      // Check the current node directly, as the TestUtility relies on the
+      // FocusManager to identify the current focus.
+      assertEquals(
+          chrome.automation.RoleType.TEXT_FIELD, Navigator.byItem.node_.role);
+      TestUtility.pressSelectSwitch();
 
-        let found = false;
-        while (!found) {
-          TestUtility.pressNextSwitch();
-          if (Navigator.byItem.node_ instanceof BackButtonNode) {
-            found = true;
-          }
+      let found = false;
+      while (!found) {
+        TestUtility.pressNextSwitch();
+        if (Navigator.byItem.node_ instanceof BackButtonNode) {
+          found = true;
         }
+      }
 
-        const rings = FocusRingManager.instance.rings_;
-        const primary = rings.get(SAConstants.Focus.ID.PRIMARY);
-        const preview = rings.get(SAConstants.Focus.ID.PREVIEW);
-        // Primary and preview focus should be empty.
-        assertEquals(0, primary.rects.length);
-        assertEquals(0, preview.rects.length);
-      });
+      const rings = FocusRingManager.instance.rings_;
+      const primary = rings.get(SAConstants.Focus.ID.PRIMARY);
+      const preview = rings.get(SAConstants.Focus.ID.PREVIEW);
+      // Primary and preview focus should be empty.
+      assertEquals(0, primary.rects.length);
+      assertEquals(0, preview.rects.length);
     });
 
-TEST_F('SwitchAccessFocusRingManagerTest', 'ButtonFocus', function() {
+TEST_F('SwitchAccessFocusRingManagerTest', 'ButtonFocus', async function() {
   const site = '<button>Test</button>';
-  this.runWithLoadedTree(site, async (rootWebArea) => {
-    const button = rootWebArea.find({role: chrome.automation.RoleType.BUTTON});
-    Navigator.byItem.moveTo_(button);
+  const rootWebArea = await this.runWithLoadedTree(site);
+  const button = rootWebArea.find({role: chrome.automation.RoleType.BUTTON});
+  Navigator.byItem.moveTo_(button);
 
-    const rings = FocusRingManager.instance.rings_;
-    const primary = rings.get(SAConstants.Focus.ID.PRIMARY);
-    const preview = rings.get(SAConstants.Focus.ID.PREVIEW);
-    assertEquals(1, primary.rects.length);
-    assertEquals(0, preview.rects.length);
-    // Primary focus should be on the button.
-    const focusLocation = primary.rects[0];
-    const buttonLocation = button.location;
-    assertTrue(RectUtil.equal(buttonLocation, focusLocation));
-  });
+  const rings = FocusRingManager.instance.rings_;
+  const primary = rings.get(SAConstants.Focus.ID.PRIMARY);
+  const preview = rings.get(SAConstants.Focus.ID.PREVIEW);
+  assertEquals(1, primary.rects.length);
+  assertEquals(0, preview.rects.length);
+  // Primary focus should be on the button.
+  const focusLocation = primary.rects[0];
+  const buttonLocation = button.location;
+  assertTrue(RectUtil.equal(buttonLocation, focusLocation));
 });
 
-TEST_F('SwitchAccessFocusRingManagerTest', 'GroupFocus', function() {
+TEST_F('SwitchAccessFocusRingManagerTest', 'GroupFocus', async function() {
   const site = `
     <div role="menu">
       <div role="menuitem">Dog</div>
       <div role="menuitem">Cat</div>
     </div>
   `;
-  this.runWithLoadedTree(site, async (rootWebArea) => {
-    const menu = rootWebArea.find({role: chrome.automation.RoleType.MENU});
-    const menuItem = rootWebArea.find({
-      role: chrome.automation.RoleType.MENU_ITEM,
-      attributes: {name: 'Dog'}
-    });
-    assertNotNullNorUndefined(menu);
-    assertNotNullNorUndefined(menuItem);
-    Navigator.byItem.moveTo_(menu);
+  const rootWebArea = await this.runWithLoadedTree(site);
+  const menu = rootWebArea.find({role: chrome.automation.RoleType.MENU});
+  const menuItem = rootWebArea.find(
+      {role: chrome.automation.RoleType.MENU_ITEM, attributes: {name: 'Dog'}});
+  assertNotNullNorUndefined(menu);
+  assertNotNullNorUndefined(menuItem);
+  Navigator.byItem.moveTo_(menu);
 
-    // Verify the number of rings.
-    const rings = FocusRingManager.instance.rings_;
-    const primary = rings.get(SAConstants.Focus.ID.PRIMARY);
-    const preview = rings.get(SAConstants.Focus.ID.PREVIEW);
-    assertEquals(1, primary.rects.length);
-    assertEquals(1, preview.rects.length);
+  // Verify the number of rings.
+  const rings = FocusRingManager.instance.rings_;
+  const primary = rings.get(SAConstants.Focus.ID.PRIMARY);
+  const preview = rings.get(SAConstants.Focus.ID.PREVIEW);
+  assertEquals(1, primary.rects.length);
+  assertEquals(1, preview.rects.length);
 
-    // Use ringNodesForTesting_ to verify the underlying nodes.
-    const ringNodes = FocusRingManager.instance.ringNodesForTesting_;
-    const primaryNode =
-        ringNodes.get(SAConstants.Focus.ID.PRIMARY).automationNode;
-    const previewNode =
-        ringNodes.get(SAConstants.Focus.ID.PREVIEW).automationNode;
+  // Use ringNodesForTesting_ to verify the underlying nodes.
+  const ringNodes = FocusRingManager.instance.ringNodesForTesting_;
+  const primaryNode =
+      ringNodes.get(SAConstants.Focus.ID.PRIMARY).automationNode;
+  const previewNode =
+      ringNodes.get(SAConstants.Focus.ID.PREVIEW).automationNode;
 
-    assertEquals(
-        menu, primaryNode,
-        'primary focus should be around the group (the menu)');
-    assertEquals(
-        menuItem, previewNode, 'preview focus should be around the menu item');
-  });
+  assertEquals(
+      menu, primaryNode, 'primary focus should be around the group (the menu)');
+  assertEquals(
+      menuItem, previewNode, 'preview focus should be around the menu item');
 });
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 71c6c04..4d297237 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
@@ -48,7 +48,7 @@
   return Navigator.byItem.node_;
 }
 
-TEST_F('SwitchAccessItemScanManagerTest', 'MoveTo', function() {
+TEST_F('SwitchAccessItemScanManagerTest', 'MoveTo', async function() {
   const website = `<div id="outerGroup">
                      <div id="group">
                        <input type="text">
@@ -56,64 +56,63 @@
                      </div>
                      <button></button>
                    </div>`;
-  this.runWithLoadedTree(website, (rootWebArea) => {
-    const desktop = rootWebArea.parent.root;
-    const textFields =
-        desktop.findAll({role: chrome.automation.RoleType.TEXT_FIELD});
-    assertTrue(
-        textFields.length === 2 || textFields.length === 3,
-        'Should be exactly 2 or 3 text fields.');
-    const omnibar = textFields[0];
-    const textInput = textFields[textFields.length - 1];
-    const sliders = desktop.findAll({role: chrome.automation.RoleType.SLIDER});
-    assertEquals(1, sliders.length, 'Should be exactly 1 slider.');
-    const slider = sliders[0];
-    const group = this.findNodeById('group');
-    const outerGroup = this.findNodeById('outerGroup');
+  const rootWebArea = await this.runWithLoadedTree(website);
+  const desktop = rootWebArea.parent.root;
+  const textFields =
+      desktop.findAll({role: chrome.automation.RoleType.TEXT_FIELD});
+  assertTrue(
+      textFields.length === 2 || textFields.length === 3,
+      'Should be exactly 2 or 3 text fields.');
+  const omnibar = textFields[0];
+  const textInput = textFields[textFields.length - 1];
+  const sliders = desktop.findAll({role: chrome.automation.RoleType.SLIDER});
+  assertEquals(1, sliders.length, 'Should be exactly 1 slider.');
+  const slider = sliders[0];
+  const group = this.findNodeById('group');
+  const outerGroup = this.findNodeById('outerGroup');
 
-    Navigator.byItem.moveTo_(omnibar);
-    assertEquals(
-        chrome.automation.RoleType.TEXT_FIELD, Navigator.byItem.node_.role,
-        'Did not successfully move to the omnibar');
-    assertFalse(
-        Navigator.byItem.group_.isEquivalentTo(group),
-        'Omnibar is in the group in page contents somehow');
-    assertFalse(
-        Navigator.byItem.group_.isEquivalentTo(outerGroup),
-        'Omnibar is in the outer group in page contents somehow');
-    const grandGroup = Navigator.byItem.history_.peek().group;
-    assertFalse(
-        grandGroup.isEquivalentTo(group),
-        'Group stack contains the group from page contents');
-    assertFalse(
-        grandGroup.isEquivalentTo(outerGroup),
-        'Group stack contains the outer group from page contents');
+  Navigator.byItem.moveTo_(omnibar);
+  assertEquals(
+      chrome.automation.RoleType.TEXT_FIELD, Navigator.byItem.node_.role,
+      'Did not successfully move to the omnibar');
+  assertFalse(
+      Navigator.byItem.group_.isEquivalentTo(group),
+      'Omnibar is in the group in page contents somehow');
+  assertFalse(
+      Navigator.byItem.group_.isEquivalentTo(outerGroup),
+      'Omnibar is in the outer group in page contents somehow');
+  const grandGroup = Navigator.byItem.history_.peek().group;
+  assertFalse(
+      grandGroup.isEquivalentTo(group),
+      'Group stack contains the group from page contents');
+  assertFalse(
+      grandGroup.isEquivalentTo(outerGroup),
+      'Group stack contains the outer group from page contents');
 
-    Navigator.byItem.moveTo_(textInput);
-    assertEquals(
-        chrome.automation.RoleType.TEXT_FIELD, Navigator.byItem.node_.role,
-        'Did not successfully move to the text input');
-    assertTrue(
-        Navigator.byItem.group_.isEquivalentTo(group),
-        'Group node was not successfully populated');
-    assertTrue(
-        Navigator.byItem.history_.peek().group.isEquivalentTo(outerGroup),
-        'History was not built properly');
+  Navigator.byItem.moveTo_(textInput);
+  assertEquals(
+      chrome.automation.RoleType.TEXT_FIELD, Navigator.byItem.node_.role,
+      'Did not successfully move to the text input');
+  assertTrue(
+      Navigator.byItem.group_.isEquivalentTo(group),
+      'Group node was not successfully populated');
+  assertTrue(
+      Navigator.byItem.history_.peek().group.isEquivalentTo(outerGroup),
+      'History was not built properly');
 
-    Navigator.byItem.moveTo_(slider);
-    assertEquals(
-        chrome.automation.RoleType.SLIDER, Navigator.byItem.node_.role,
-        'Did not successfully move to the slider');
+  Navigator.byItem.moveTo_(slider);
+  assertEquals(
+      chrome.automation.RoleType.SLIDER, Navigator.byItem.node_.role,
+      'Did not successfully move to the slider');
 
-    Navigator.byItem.moveTo_(group);
-    assertTrue(Navigator.byItem.node_.isGroup(), 'Current node is not a group');
-    assertTrue(
-        Navigator.byItem.node_.isEquivalentTo(group),
-        'Did not find the right group');
-  });
+  Navigator.byItem.moveTo_(group);
+  assertTrue(Navigator.byItem.node_.isGroup(), 'Current node is not a group');
+  assertTrue(
+      Navigator.byItem.node_.isEquivalentTo(group),
+      'Did not find the right group');
 });
 
-TEST_F('SwitchAccessItemScanManagerTest', 'JumpTo', function() {
+TEST_F('SwitchAccessItemScanManagerTest', 'JumpTo', async function() {
   const website = `<div id="group1">
                      <input id="testinput" type="text">
                      <button></button>
@@ -122,37 +121,36 @@
                      <button></button>
                      <button></button>
                    </div>`;
-  this.runWithLoadedTree(website, (rootWebArea) => {
-    const desktop = rootWebArea.parent.root;
-    const textInput = this.findNodeById('testinput');
-    assertNotNullNorUndefined(textInput, 'Text field is undefined');
-    const group1 = this.findNodeById('group1');
-    const group2 = this.findNodeById('group2');
+  const rootWebArea = await this.runWithLoadedTree(website);
+  const desktop = rootWebArea.parent.root;
+  const textInput = this.findNodeById('testinput');
+  assertNotNullNorUndefined(textInput, 'Text field is undefined');
+  const group1 = this.findNodeById('group1');
+  const group2 = this.findNodeById('group2');
 
-    Navigator.byItem.moveTo_(textInput);
-    const textInputNode = Navigator.byItem.node_;
-    assertEquals(
-        chrome.automation.RoleType.TEXT_FIELD, textInputNode.role,
-        'Did not successfully move to the text input');
-    assertTrue(
-        Navigator.byItem.group_.isEquivalentTo(group1),
-        'Group is initialized in an unexpected manner');
+  Navigator.byItem.moveTo_(textInput);
+  const textInputNode = Navigator.byItem.node_;
+  assertEquals(
+      chrome.automation.RoleType.TEXT_FIELD, textInputNode.role,
+      'Did not successfully move to the text input');
+  assertTrue(
+      Navigator.byItem.group_.isEquivalentTo(group1),
+      'Group is initialized in an unexpected manner');
 
-    Navigator.byItem.jumpTo_(BasicRootNode.buildTree(group2));
-    assertFalse(
-        Navigator.byItem.group_.isEquivalentTo(group1), 'Jump did nothing');
-    assertTrue(
-        Navigator.byItem.group_.isEquivalentTo(group2),
-        'Jumped to the wrong group');
+  Navigator.byItem.jumpTo_(BasicRootNode.buildTree(group2));
+  assertFalse(
+      Navigator.byItem.group_.isEquivalentTo(group1), 'Jump did nothing');
+  assertTrue(
+      Navigator.byItem.group_.isEquivalentTo(group2),
+      'Jumped to the wrong group');
 
-    Navigator.byItem.exitGroup_();
-    assertTrue(
-        Navigator.byItem.group_.isEquivalentTo(group1),
-        'Did not jump back to the right group.');
-  });
+  Navigator.byItem.exitGroup_();
+  assertTrue(
+      Navigator.byItem.group_.isEquivalentTo(group1),
+      'Did not jump back to the right group.');
 });
 
-TEST_F('SwitchAccessItemScanManagerTest', 'SelectButton', function() {
+TEST_F('SwitchAccessItemScanManagerTest', 'SelectButton', async function() {
   const website = `<button id="test" aria-pressed=false>First Button</button>
       <button>Second Button</button>
       <script>
@@ -164,380 +162,375 @@
         };
       </script>`;
 
-  this.runWithLoadedTree(website, function(pageContents) {
-    this.moveToPageContents(pageContents);
+  const pageContents = await this.runWithLoadedTree(website);
+  this.moveToPageContents(pageContents);
 
-    const node = currentNode().automationNode;
-    assertNotNullNorUndefined(node, 'Node is invalid');
-    assertEquals(node.name, 'First Button', 'Did not find the right node');
+  const node = currentNode().automationNode;
+  assertNotNullNorUndefined(node, 'Node is invalid');
+  assertEquals(node.name, 'First Button', 'Did not find the right node');
 
-    node.addEventListener(
-        chrome.automation.EventType.CHECKED_STATE_CHANGED,
-        this.newCallback((event) => {
-          assertEquals(
-              node.name, event.target.name,
-              'Checked state changed on unexpected node');
-        }));
+  node.addEventListener(
+      chrome.automation.EventType.CHECKED_STATE_CHANGED,
+      this.newCallback((event) => {
+        assertEquals(
+            node.name, event.target.name,
+            'Checked state changed on unexpected node');
+      }));
 
-    Navigator.byItem.node_.performAction('select');
-  });
+  Navigator.byItem.node_.performAction('select');
 });
 
-TEST_F('SwitchAccessItemScanManagerTest', 'EnterGroup', function() {
+TEST_F('SwitchAccessItemScanManagerTest', 'EnterGroup', async function() {
   const website = `<div id="group">
                      <button></button>
                      <button></button>
                    </div>
                    <input type="range">`;
-  this.runWithLoadedTree(website, (rootWebArea) => {
-    const targetGroup = this.findNodeById('group');
-    Navigator.byItem.moveTo_(targetGroup);
+  const rootWebArea = await this.runWithLoadedTree(website);
+  const targetGroup = this.findNodeById('group');
+  Navigator.byItem.moveTo_(targetGroup);
 
-    const originalGroup = Navigator.byItem.group_;
-    assertEquals(
-        Navigator.byItem.node_.automationNode.htmlAttributes.id, 'group',
-        'Did not move to group properly');
+  const originalGroup = Navigator.byItem.group_;
+  assertEquals(
+      Navigator.byItem.node_.automationNode.htmlAttributes.id, 'group',
+      'Did not move to group properly');
 
-    Navigator.byItem.enterGroup();
-    assertEquals(
-        chrome.automation.RoleType.BUTTON, Navigator.byItem.node_.role,
-        'Current node is not a button');
-    assertTrue(
-        Navigator.byItem.group_.isEquivalentTo(targetGroup),
-        'Target group was not entered');
+  Navigator.byItem.enterGroup();
+  assertEquals(
+      chrome.automation.RoleType.BUTTON, Navigator.byItem.node_.role,
+      'Current node is not a button');
+  assertTrue(
+      Navigator.byItem.group_.isEquivalentTo(targetGroup),
+      'Target group was not entered');
 
-    Navigator.byItem.exitGroup_();
-    assertTrue(
-        originalGroup.equals(Navigator.byItem.group_),
-        'Did not move back to the original group');
-  });
+  Navigator.byItem.exitGroup_();
+  assertTrue(
+      originalGroup.equals(Navigator.byItem.group_),
+      'Did not move back to the original group');
 });
 
-TEST_F('SwitchAccessItemScanManagerTest', 'MoveForward', function() {
+TEST_F('SwitchAccessItemScanManagerTest', 'MoveForward', async function() {
   const website = `<div>
                      <button id="button1"></button>
                      <button id="button2"></button>
                      <button id="button3"></button>
                    </div>`;
-  this.runWithLoadedTree(website, (rootWebArea) => {
-    Navigator.byItem.moveTo_(this.findNodeById('button1'));
-    const button1 = Navigator.byItem.node_;
-    assertFalse(
-        button1 instanceof BackButtonNode,
-        'button1 should not be a BackButtonNode');
-    assertEquals(
-        'button1', button1.automationNode.htmlAttributes.id,
-        'Current node is not button1');
+  const rootWebArea = await this.runWithLoadedTree(website);
+  Navigator.byItem.moveTo_(this.findNodeById('button1'));
+  const button1 = Navigator.byItem.node_;
+  assertFalse(
+      button1 instanceof BackButtonNode,
+      'button1 should not be a BackButtonNode');
+  assertEquals(
+      'button1', button1.automationNode.htmlAttributes.id,
+      'Current node is not button1');
 
-    Navigator.byItem.moveForward();
-    assertFalse(
-        button1.equals(Navigator.byItem.node_),
-        'Still on button1 after moveForward()');
-    const button2 = Navigator.byItem.node_;
-    assertFalse(
-        button2 instanceof BackButtonNode,
-        'button2 should not be a BackButtonNode');
-    assertEquals(
-        'button2', button2.automationNode.htmlAttributes.id,
-        'Current node is not button2');
+  Navigator.byItem.moveForward();
+  assertFalse(
+      button1.equals(Navigator.byItem.node_),
+      'Still on button1 after moveForward()');
+  const button2 = Navigator.byItem.node_;
+  assertFalse(
+      button2 instanceof BackButtonNode,
+      'button2 should not be a BackButtonNode');
+  assertEquals(
+      'button2', button2.automationNode.htmlAttributes.id,
+      'Current node is not button2');
 
-    Navigator.byItem.moveForward();
-    assertFalse(
-        button1.equals(Navigator.byItem.node_),
-        'Unexpected navigation to button1');
-    assertFalse(
-        button2.equals(Navigator.byItem.node_),
-        'Still on button2 after moveForward()');
-    const button3 = Navigator.byItem.node_;
-    assertFalse(
-        button3 instanceof BackButtonNode,
-        'button3 should not be a BackButtonNode');
-    assertEquals(
-        'button3', button3.automationNode.htmlAttributes.id,
-        'Current node is not button3');
+  Navigator.byItem.moveForward();
+  assertFalse(
+      button1.equals(Navigator.byItem.node_),
+      'Unexpected navigation to button1');
+  assertFalse(
+      button2.equals(Navigator.byItem.node_),
+      'Still on button2 after moveForward()');
+  const button3 = Navigator.byItem.node_;
+  assertFalse(
+      button3 instanceof BackButtonNode,
+      'button3 should not be a BackButtonNode');
+  assertEquals(
+      'button3', button3.automationNode.htmlAttributes.id,
+      'Current node is not button3');
 
-    Navigator.byItem.moveForward();
-    assertTrue(
-        Navigator.byItem.node_ instanceof BackButtonNode,
-        'BackButtonNode should come after button3');
+  Navigator.byItem.moveForward();
+  assertTrue(
+      Navigator.byItem.node_ instanceof BackButtonNode,
+      'BackButtonNode should come after button3');
 
-    Navigator.byItem.moveForward();
-    assertTrue(
-        button1.equals(Navigator.byItem.node_),
-        'button1 should come after the BackButtonNode');
-  });
+  Navigator.byItem.moveForward();
+  assertTrue(
+      button1.equals(Navigator.byItem.node_),
+      'button1 should come after the BackButtonNode');
 });
 
-TEST_F('SwitchAccessItemScanManagerTest', 'MoveBackward', function() {
+TEST_F('SwitchAccessItemScanManagerTest', 'MoveBackward', async function() {
   const website = `<div>
                      <button id="button1"></button>
                      <button id="button2"></button>
                      <button id="button3"></button>
                    </div>`;
-  this.runWithLoadedTree(website, (rootWebArea) => {
-    Navigator.byItem.moveTo_(this.findNodeById('button1'));
-    const button1 = Navigator.byItem.node_;
-    assertFalse(
-        button1 instanceof BackButtonNode,
-        'button1 should not be a BackButtonNode');
-    assertEquals(
-        'button1', button1.automationNode.htmlAttributes.id,
-        'Current node is not button1');
+  const rootWebArea = await this.runWithLoadedTree(website);
+  Navigator.byItem.moveTo_(this.findNodeById('button1'));
+  const button1 = Navigator.byItem.node_;
+  assertFalse(
+      button1 instanceof BackButtonNode,
+      'button1 should not be a BackButtonNode');
+  assertEquals(
+      'button1', button1.automationNode.htmlAttributes.id,
+      'Current node is not button1');
 
-    Navigator.byItem.moveBackward();
-    assertTrue(
-        Navigator.byItem.node_ instanceof BackButtonNode,
-        'BackButtonNode should come before button1');
+  Navigator.byItem.moveBackward();
+  assertTrue(
+      Navigator.byItem.node_ instanceof BackButtonNode,
+      'BackButtonNode should come before button1');
 
-    Navigator.byItem.moveBackward();
-    assertFalse(
-        button1.equals(Navigator.byItem.node_),
-        'Unexpected navigation to button1');
-    const button3 = Navigator.byItem.node_;
-    assertFalse(
-        button3 instanceof BackButtonNode,
-        'button3 should not be a BackButtonNode');
-    assertEquals(
-        'button3', button3.automationNode.htmlAttributes.id,
-        'Current node is not button3');
+  Navigator.byItem.moveBackward();
+  assertFalse(
+      button1.equals(Navigator.byItem.node_),
+      'Unexpected navigation to button1');
+  const button3 = Navigator.byItem.node_;
+  assertFalse(
+      button3 instanceof BackButtonNode,
+      'button3 should not be a BackButtonNode');
+  assertEquals(
+      'button3', button3.automationNode.htmlAttributes.id,
+      'Current node is not button3');
 
-    Navigator.byItem.moveBackward();
-    assertFalse(
-        button3.equals(Navigator.byItem.node_),
-        'Still on button3 after moveBackward()');
-    assertFalse(button1.equals(Navigator.byItem.node_), 'Skipped button2');
-    const button2 = Navigator.byItem.node_;
-    assertFalse(
-        button2 instanceof BackButtonNode,
-        'button2 should not be a BackButtonNode');
-    assertEquals(
-        'button2', button2.automationNode.htmlAttributes.id,
-        'Current node is not button2');
+  Navigator.byItem.moveBackward();
+  assertFalse(
+      button3.equals(Navigator.byItem.node_),
+      'Still on button3 after moveBackward()');
+  assertFalse(button1.equals(Navigator.byItem.node_), 'Skipped button2');
+  const button2 = Navigator.byItem.node_;
+  assertFalse(
+      button2 instanceof BackButtonNode,
+      'button2 should not be a BackButtonNode');
+  assertEquals(
+      'button2', button2.automationNode.htmlAttributes.id,
+      'Current node is not button2');
 
-    Navigator.byItem.moveBackward();
-    assertTrue(
-        button1.equals(Navigator.byItem.node_),
-        'button1 should come before button2');
-  });
+  Navigator.byItem.moveBackward();
+  assertTrue(
+      button1.equals(Navigator.byItem.node_),
+      'button1 should come before button2');
 });
 
 TEST_F(
     'SwitchAccessItemScanManagerTest', 'NodeUndefinedBeforeTreeChangeRemoved',
-    function() {
+    async function() {
       const website = `<div>
                      <button id="button1"></button>
                    </div>`;
-      this.runWithLoadedTree(website, (rootWebArea) => {
-        Navigator.byItem.moveTo_(this.findNodeById('button1'));
-        const button1 = Navigator.byItem.node_;
-        assertFalse(
-            button1 instanceof BackButtonNode,
-            'button1 should not be a BackButtonNode');
-        assertEquals(
-            'button1', button1.automationNode.htmlAttributes.id,
-            'Current node is not button1');
+      const rootWebArea = await this.runWithLoadedTree(website);
+      Navigator.byItem.moveTo_(this.findNodeById('button1'));
+      const button1 = Navigator.byItem.node_;
+      assertFalse(
+          button1 instanceof BackButtonNode,
+          'button1 should not be a BackButtonNode');
+      assertEquals(
+          'button1', button1.automationNode.htmlAttributes.id,
+          'Current node is not button1');
 
-        // Simulate the underlying node's deletion. Note that this is different
-        // than an orphaned node (which can have a valid AutomationNode
-        // instance, but no backing C++ object, so attributes returned like role
-        // are undefined).
-        Navigator.byItem.node_.baseNode_ = undefined;
+      // Simulate the underlying node's deletion. Note that this is different
+      // than an orphaned node (which can have a valid AutomationNode
+      // instance, but no backing C++ object, so attributes returned like role
+      // are undefined).
+      Navigator.byItem.node_.baseNode_ = undefined;
 
-        // Tree change removed gets sent by C++ after the tree has already
-        // applied changes (so this comes after the above clearing).
-        Navigator.byItem.onTreeChange_(
-            {type: chrome.automation.TreeChangeType.NODE_REMOVED});
-      });
+      // Tree change removed gets sent by C++ after the tree has already
+      // applied changes (so this comes after the above clearing).
+      Navigator.byItem.onTreeChange_(
+          {type: chrome.automation.TreeChangeType.NODE_REMOVED});
     });
 
 TEST_F(
     'SwitchAccessItemScanManagerTest', 'ScanAndTypeVirtualKeyboard',
-    function() {
+    async function() {
       const website = `<input type="text" id="testinput"></input>`;
-      this.runWithLoadedTree(website, async (rootWebArea) => {
-        // SA initially focuses this node; wait for it first.
+      const rootWebArea = await this.runWithLoadedTree(website);
+      // SA initially focuses this node; 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'));
+      const input = Navigator.byItem.node_;
+      assertEquals(
+          'testinput', input.automationNode.htmlAttributes.id,
+          'Current node is not input');
+      input.performAction(SwitchAccessMenuAction.KEYBOARD);
+
+      const keyboard =
+          await this.untilFocusIs({role: chrome.automation.RoleType.KEYBOARD});
+      keyboard.performAction('select');
+
+      const key = await this.untilFocusIs({instance: KeyboardNode});
+
+      key.performAction('select');
+
+      if (input.automationNode.value !== 'q') {
+        // Wait for the potential value change.
         await new Promise(resolve => {
-          chrome.commandLinePrivate.hasSwitch(
-              'lacros-chrome-path', async hasLacrosChromePath => {
-                if (!hasLacrosChromePath) {
-                  await this.untilFocusIs(
-                      {className: 'BrowserNonClientFrameViewChromeOS'});
+          input.automationNode.addEventListener(
+              chrome.automation.EventType.VALUE_CHANGED, (event) => {
+                if (event.target.value === 'q') {
+                  resolve();
                 }
-                resolve();
               });
         });
-
-        // Move to the text field.
-        Navigator.byItem.moveTo_(this.findNodeById('testinput'));
-        const input = Navigator.byItem.node_;
-        assertEquals(
-            'testinput', input.automationNode.htmlAttributes.id,
-            'Current node is not input');
-        input.performAction(SwitchAccessMenuAction.KEYBOARD);
-
-        const keyboard = await this.untilFocusIs(
-            {role: chrome.automation.RoleType.KEYBOARD});
-        keyboard.performAction('select');
-
-        const key = await this.untilFocusIs({instance: KeyboardNode});
-
-        key.performAction('select');
-
-        if (input.automationNode.value !== 'q') {
-          // Wait for the potential value change.
-          await new Promise(resolve => {
-            input.automationNode.addEventListener(
-                chrome.automation.EventType.VALUE_CHANGED, (event) => {
-                  if (event.target.value === 'q') {
-                    resolve();
-                  }
-                });
-          });
-        }
-      });
+      }
     });
 
-TEST_F('SwitchAccessItemScanManagerTest', 'DismissVirtualKeyboard', function() {
-  const website =
-      `<input type="text" id="testinput"></input><button>ok</button>`;
-  this.runWithLoadedTree(website, async (rootWebArea) => {
-    // 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'));
-    const input = Navigator.byItem.node_;
-    assertEquals(
-        'testinput', input.automationNode.htmlAttributes.id,
-        'Current node is not input');
-    input.performAction(SwitchAccessMenuAction.KEYBOARD);
-
-    const keyboard =
-        await this.untilFocusIs({role: chrome.automation.RoleType.KEYBOARD});
-    keyboard.performAction('select');
-
-    // Grab the key.
-    const key = await this.untilFocusIs({instance: KeyboardNode});
-
-    // Simulate a page focusing the ok button.
-    const okButton = rootWebArea.find({attributes: {name: 'ok'}});
-    okButton.focus();
-
-    // 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 => {
-      keyboard.automationNode.addEventListener(
-          chrome.automation.EventType.STATE_CHANGED, (event) => {
-            if (event.target.role === chrome.automation.RoleType.KEYBOARD &&
-                event.target.state.invisible) {
+TEST_F(
+    'SwitchAccessItemScanManagerTest', 'DismissVirtualKeyboard',
+    async function() {
+      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();
-            }
-          });
-    });
+            });
+      });
 
-    // We should end up back on the focused button in SA.
-    const button =
-        await this.untilFocusIs({role: chrome.automation.RoleType.BUTTON});
-    assertEquals('ok', button.automationNode.name);
-  });
-});
+      // Move to the text field.
+      Navigator.byItem.moveTo_(this.findNodeById('testinput'));
+      const input = Navigator.byItem.node_;
+      assertEquals(
+          'testinput', input.automationNode.htmlAttributes.id,
+          'Current node is not input');
+      input.performAction(SwitchAccessMenuAction.KEYBOARD);
+
+      const keyboard =
+          await this.untilFocusIs({role: chrome.automation.RoleType.KEYBOARD});
+      keyboard.performAction('select');
+
+      // Grab the key.
+      const key = await this.untilFocusIs({instance: KeyboardNode});
+
+      // Simulate a page focusing the ok button.
+      const okButton = rootWebArea.find({attributes: {name: 'ok'}});
+      okButton.focus();
+
+      // 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 => {
+        keyboard.automationNode.addEventListener(
+            chrome.automation.EventType.STATE_CHANGED, (event) => {
+              if (event.target.role === chrome.automation.RoleType.KEYBOARD &&
+                  event.target.state.invisible) {
+                resolve();
+              }
+            });
+      });
+
+      // We should end up back on the focused button in SA.
+      const button =
+          await this.untilFocusIs({role: chrome.automation.RoleType.BUTTON});
+      assertEquals('ok', button.automationNode.name);
+    });
 
 // TODO(crbug.com/1260231): Test is flaky.
 TEST_F(
     'SwitchAccessItemScanManagerTest', 'DISABLED_ChildrenChangedDoesNotRefresh',
-    function() {
+    async function() {
       const website = `
     <div id="slider" role="slider">
       <div role="group"><div></div></div>
     </div>
     <button>done</button>
   `;
-      this.runWithLoadedTree(website, async (rootWebArea) => {
-        // 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 slider.
-        Navigator.byItem.moveTo_(this.findNodeById('slider'));
-        const slider = Navigator.byItem.node_;
-        assertEquals(
-            'slider', slider.automationNode.htmlAttributes.id,
-            'Current node is not slider');
-
-        // Trigger a children changed on the group.
-        const automationGroup =
-            rootWebArea.find({role: chrome.automation.RoleType.GROUP});
-        assertTrue(!!automationGroup);
-        const group = Navigator.byItem.group_;
-        assertTrue(!!group);
-        const handler = group.childrenChangedHandler_;
-        assertTrue(!!handler);
-
-        // Fake a children changed event.
-        handler.eventStack_ = [{
-          type: chrome.automation.EventType.CHILDREN_CHANGED,
-          target: automationGroup
-        }];
-        handler.handleEvent_();
-
-        // This subtree is not interesting, so it should not have triggered a
-        // complete refresh of the SA tree.
-        assertEquals(slider, Navigator.byItem.node_);
+      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 slider.
+      Navigator.byItem.moveTo_(this.findNodeById('slider'));
+      const slider = Navigator.byItem.node_;
+      assertEquals(
+          'slider', slider.automationNode.htmlAttributes.id,
+          'Current node is not slider');
+
+      // Trigger a children changed on the group.
+      const automationGroup =
+          rootWebArea.find({role: chrome.automation.RoleType.GROUP});
+      assertTrue(!!automationGroup);
+      const group = Navigator.byItem.group_;
+      assertTrue(!!group);
+      const handler = group.childrenChangedHandler_;
+      assertTrue(!!handler);
+
+      // Fake a children changed event.
+      handler.eventStack_ = [{
+        type: chrome.automation.EventType.CHILDREN_CHANGED,
+        target: automationGroup
+      }];
+      handler.handleEvent_();
+
+      // This subtree is not interesting, so it should not have triggered a
+      // complete refresh of the SA tree.
+      assertEquals(slider, Navigator.byItem.node_);
     });
 
-TEST_F('SwitchAccessItemScanManagerTest', 'InitialFocus', function() {
+TEST_F('SwitchAccessItemScanManagerTest', 'InitialFocus', async function() {
   const website = `<input></input><button autofocus></button>`;
-  this.runWithLoadedTree(website, async (rootWebArea) => {
-    // The button should have initial focus. This ensures we move past the focus
-    // event below.
-    const button =
-        await this.untilFocusIs({role: chrome.automation.RoleType.BUTTON});
+  const rootWebArea = await this.runWithLoadedTree(website);
+  // The button should have initial focus. This ensures we move past the focus
+  // event below.
+  const button =
+      await this.untilFocusIs({role: chrome.automation.RoleType.BUTTON});
 
-    // Build a new ItemScanManager to see what it sets as the initial node.
-    const desktop = rootWebArea.parent.root;
-    assertEquals(
-        chrome.automation.RoleType.DESKTOP, desktop.role,
-        `Unexpected desktop ${desktop.toString()}`);
-    const manager = new ItemScanManager(desktop);
-    assertEquals(
-        button.automationNode, manager.node_.automationNode,
-        `Unexpected focus ${manager.node_.debugString()}`);
-  });
+  // Build a new ItemScanManager to see what it sets as the initial node.
+  const desktop = rootWebArea.parent.root;
+  assertEquals(
+      chrome.automation.RoleType.DESKTOP, desktop.role,
+      `Unexpected desktop ${desktop.toString()}`);
+  const manager = new ItemScanManager(desktop);
+  assertEquals(
+      button.automationNode, manager.node_.automationNode,
+      `Unexpected focus ${manager.node_.debugString()}`);
 });
 
 
-TEST_F('SwitchAccessItemScanManagerTest', 'SyncFocusToNewWindow', function() {
-  const website1 = `<button autofocus>one</button>`;
-  const website2 = `<button autofocus>two</button>`;
-  this.runWithLoadedTree(website1, async (rootWebArea) => {
-    // Wait for the first button to get SA focused.
-    const button1 = await this.untilFocusIs(
-        {role: chrome.automation.RoleType.BUTTON, name: 'one'});
+TEST_F(
+    'SwitchAccessItemScanManagerTest', 'SyncFocusToNewWindow',
+    async function() {
+      const website1 = `<button autofocus>one</button>`;
+      const website2 = `<button autofocus>two</button>`;
+      await this.runWithLoadedTree(website1);
+      // Wait for the first button to get SA focused.
+      const button1 = await this.untilFocusIs(
+          {role: chrome.automation.RoleType.BUTTON, name: 'one'});
 
-    // Launch a new browser window and load up the second site.
-    EventGenerator.sendKeyPress(KeyCode.N, {ctrl: true});
-    this.runWithLoadedTree(website2, async (rootWebArea) => {
+      // Launch a new browser window and load up the second site.
+      EventGenerator.sendKeyPress(KeyCode.N, {ctrl: true});
+      await this.runWithLoadedTree(website2);
       // Wait for the second button to get SA focused.
       const button2 = await this.untilFocusIs(
           {role: chrome.automation.RoleType.BUTTON, name: 'two'});
@@ -594,52 +587,49 @@
       });
       assertEquals(currentFocus, button2.automationNode);
     });
-  });
-});
 
 // TODO(crbug.com/1219067): Unflake.
 TEST_F(
     'SwitchAccessItemScanManagerTest', 'DISABLED_LockScreenBlocksUserSession',
-    function() {
+    async function() {
       const website = `<button autofocus>kitties!</button>`;
-      this.runWithLoadedTree(website, async (rootWebArea) => {
-        let button =
-            await this.untilFocusIs({role: chrome.automation.RoleType.BUTTON});
-        assertEquals('kitties!', button.automationNode.name);
+      await this.runWithLoadedTree(website);
+      let button =
+          await this.untilFocusIs({role: chrome.automation.RoleType.BUTTON});
+      assertEquals('kitties!', button.automationNode.name);
 
-        // Lock the screen.
-        EventGenerator.sendKeyPress(KeyCode.L, {search: true});
+      // Lock the screen.
+      EventGenerator.sendKeyPress(KeyCode.L, {search: true});
 
-        // Wait for focus to move to the password field.
-        await this.untilFocusIs({
-          role: chrome.automation.RoleType.TEXT_FIELD,
-          name: 'Password for stub-user@example.com'
-        });
-
-        // The button is no longer in the tree because the screen is locked.
-        const predicate = (node) => node.name === 'kitties!' &&
-            node.role === chrome.automation.RoleType.BUTTON;
-        assertNotNullNorUndefined(
-            this.desktop_, 'this.desktop_ is null or undefined.');
-        const treeWalker = new AutomationTreeWalker(
-            this.desktop_, constants.Dir.FORWARD, {visit: predicate});
-        const node = treeWalker.next().node;
-        assertEquals(null, node);
-
-        // Log in again and confirm that the button is back and gets focus
-        // again.
-        EventGenerator.sendKeyPress(KeyCode.T);
-        EventGenerator.sendKeyPress(KeyCode.E);
-        EventGenerator.sendKeyPress(KeyCode.S);
-        EventGenerator.sendKeyPress(KeyCode.T);
-        EventGenerator.sendKeyPress(KeyCode.ZERO);
-        EventGenerator.sendKeyPress(KeyCode.ZERO);
-        EventGenerator.sendKeyPress(KeyCode.ZERO);
-        EventGenerator.sendKeyPress(KeyCode.ZERO);
-        EventGenerator.sendKeyPress(KeyCode.RETURN);
-
-        button =
-            await this.untilFocusIs({role: chrome.automation.RoleType.BUTTON});
-        assertEquals('kitties!', button.automationNode.name);
+      // Wait for focus to move to the password field.
+      await this.untilFocusIs({
+        role: chrome.automation.RoleType.TEXT_FIELD,
+        name: 'Password for stub-user@example.com'
       });
+
+      // The button is no longer in the tree because the screen is locked.
+      const predicate = (node) => node.name === 'kitties!' &&
+          node.role === chrome.automation.RoleType.BUTTON;
+      assertNotNullNorUndefined(
+          this.desktop_, 'this.desktop_ is null or undefined.');
+      const treeWalker = new AutomationTreeWalker(
+          this.desktop_, constants.Dir.FORWARD, {visit: predicate});
+      const node = treeWalker.next().node;
+      assertEquals(null, node);
+
+      // Log in again and confirm that the button is back and gets focus
+      // again.
+      EventGenerator.sendKeyPress(KeyCode.T);
+      EventGenerator.sendKeyPress(KeyCode.E);
+      EventGenerator.sendKeyPress(KeyCode.S);
+      EventGenerator.sendKeyPress(KeyCode.T);
+      EventGenerator.sendKeyPress(KeyCode.ZERO);
+      EventGenerator.sendKeyPress(KeyCode.ZERO);
+      EventGenerator.sendKeyPress(KeyCode.ZERO);
+      EventGenerator.sendKeyPress(KeyCode.ZERO);
+      EventGenerator.sendKeyPress(KeyCode.RETURN);
+
+      button =
+          await this.untilFocusIs({role: chrome.automation.RoleType.BUTTON});
+      assertEquals('kitties!', button.automationNode.name);
     });
diff --git a/chrome/browser/resources/chromeos/accessibility/switch_access/nodes/basic_node_test.js b/chrome/browser/resources/chromeos/accessibility/switch_access/nodes/basic_node_test.js
index c8802f4..a825fd4 100644
--- a/chrome/browser/resources/chromeos/accessibility/switch_access/nodes/basic_node_test.js
+++ b/chrome/browser/resources/chromeos/accessibility/switch_access/nodes/basic_node_test.js
@@ -20,7 +20,7 @@
   }
 };
 
-TEST_F('SwitchAccessBasicNodeTest', 'AsRootNode', function() {
+TEST_F('SwitchAccessBasicNodeTest', 'AsRootNode', async function() {
   const website = `<div aria-label="outer">
                      <div aria-label="inner">
                        <input type="range">
@@ -28,30 +28,29 @@
                      </div>
                      <button></button>
                    </div>`;
-  this.runWithLoadedTree(website, (rootWebArea) => {
-    const slider = rootWebArea.find({role: chrome.automation.RoleType.SLIDER});
-    const inner = slider.parent;
-    assertNotEquals(undefined, inner, 'Could not find inner group');
-    const outer = inner.parent;
-    assertNotEquals(undefined, outer, 'Could not find outer group');
+  const rootWebArea = await this.runWithLoadedTree(website);
+  const slider = rootWebArea.find({role: chrome.automation.RoleType.SLIDER});
+  const inner = slider.parent;
+  assertNotEquals(undefined, inner, 'Could not find inner group');
+  const outer = inner.parent;
+  assertNotEquals(undefined, outer, 'Could not find outer group');
 
-    const outerRootNode = BasicRootNode.buildTree(outer, null);
-    const innerNode = outerRootNode.firstChild;
-    assertTrue(innerNode.isGroup(), 'Inner group node is not a group');
+  const outerRootNode = BasicRootNode.buildTree(outer, null);
+  const innerNode = outerRootNode.firstChild;
+  assertTrue(innerNode.isGroup(), 'Inner group node is not a group');
 
-    const innerRootNode = innerNode.asRootNode();
-    assertEquals(3, innerRootNode.children.length, 'Expected 3 children');
-    const sliderNode = innerRootNode.firstChild;
-    assertEquals(
-        chrome.automation.RoleType.SLIDER, sliderNode.role,
-        'First child should be a slider');
-    assertEquals(
-        chrome.automation.RoleType.BUTTON, sliderNode.next.role,
-        'Second child should be a button');
-    assertTrue(
-        innerRootNode.lastChild instanceof BackButtonNode,
-        'Final child should be the back button');
-  });
+  const innerRootNode = innerNode.asRootNode();
+  assertEquals(3, innerRootNode.children.length, 'Expected 3 children');
+  const sliderNode = innerRootNode.firstChild;
+  assertEquals(
+      chrome.automation.RoleType.SLIDER, sliderNode.role,
+      'First child should be a slider');
+  assertEquals(
+      chrome.automation.RoleType.BUTTON, sliderNode.next.role,
+      'Second child should be a button');
+  assertTrue(
+      innerRootNode.lastChild instanceof BackButtonNode,
+      'Final child should be the back button');
 });
 
 TEST_F('SwitchAccessBasicNodeTest', 'Equals', function() {
@@ -124,57 +123,56 @@
   });
 });
 
-TEST_F('SwitchAccessBasicNodeTest', 'Actions', function() {
+TEST_F('SwitchAccessBasicNodeTest', 'Actions', async function() {
   const website = `<input type="text">
                    <button></button>
                    <input type="range" min=1 max=5 value=3>`;
-  this.runWithLoadedTree(website, (rootWebArea) => {
-    const textField = BasicNode.create(
-        rootWebArea.find({role: chrome.automation.RoleType.TEXT_FIELD}),
-        new SARootNode());
+  const rootWebArea = await this.runWithLoadedTree(website);
+  const textField = BasicNode.create(
+      rootWebArea.find({role: chrome.automation.RoleType.TEXT_FIELD}),
+      new SARootNode());
 
-    assertEquals(
-        chrome.automation.RoleType.TEXT_FIELD, textField.role,
-        'Text field node is not a text field');
-    assertTrue(
-        textField.hasAction(SwitchAccessMenuAction.KEYBOARD),
-        'Text field does not have action KEYBOARD');
-    assertTrue(
-        textField.hasAction(SwitchAccessMenuAction.DICTATION),
-        'Text field does not have action DICTATION');
-    assertFalse(
-        textField.hasAction(SwitchAccessMenuAction.SELECT),
-        'Text field has action SELECT');
+  assertEquals(
+      chrome.automation.RoleType.TEXT_FIELD, textField.role,
+      'Text field node is not a text field');
+  assertTrue(
+      textField.hasAction(SwitchAccessMenuAction.KEYBOARD),
+      'Text field does not have action KEYBOARD');
+  assertTrue(
+      textField.hasAction(SwitchAccessMenuAction.DICTATION),
+      'Text field does not have action DICTATION');
+  assertFalse(
+      textField.hasAction(SwitchAccessMenuAction.SELECT),
+      'Text field has action SELECT');
 
-    const button = BasicNode.create(
-        rootWebArea.find({role: chrome.automation.RoleType.BUTTON}),
-        new SARootNode());
+  const button = BasicNode.create(
+      rootWebArea.find({role: chrome.automation.RoleType.BUTTON}),
+      new SARootNode());
 
-    assertEquals(
-        chrome.automation.RoleType.BUTTON, button.role,
-        'Button node is not a button');
-    assertTrue(
-        button.hasAction(SwitchAccessMenuAction.SELECT),
-        'Button does not have action SELECT');
-    assertFalse(
-        button.hasAction(SwitchAccessMenuAction.KEYBOARD),
-        'Button has action KEYBOARD');
-    assertFalse(
-        button.hasAction(SwitchAccessMenuAction.DICTATION),
-        'Button has action DICTATION');
+  assertEquals(
+      chrome.automation.RoleType.BUTTON, button.role,
+      'Button node is not a button');
+  assertTrue(
+      button.hasAction(SwitchAccessMenuAction.SELECT),
+      'Button does not have action SELECT');
+  assertFalse(
+      button.hasAction(SwitchAccessMenuAction.KEYBOARD),
+      'Button has action KEYBOARD');
+  assertFalse(
+      button.hasAction(SwitchAccessMenuAction.DICTATION),
+      'Button has action DICTATION');
 
-    const slider = BasicNode.create(
-        rootWebArea.find({role: chrome.automation.RoleType.SLIDER}),
-        new SARootNode());
+  const slider = BasicNode.create(
+      rootWebArea.find({role: chrome.automation.RoleType.SLIDER}),
+      new SARootNode());
 
-    assertEquals(
-        chrome.automation.RoleType.SLIDER, slider.role,
-        'Slider node is not a slider');
-    assertTrue(
-        slider.hasAction(SwitchAccessMenuAction.INCREMENT),
-        'Slider does not have action INCREMENT');
-    assertTrue(
-        slider.hasAction(SwitchAccessMenuAction.DECREMENT),
-        'Slider does not have action DECREMENT');
-  });
+  assertEquals(
+      chrome.automation.RoleType.SLIDER, slider.role,
+      'Slider node is not a slider');
+  assertTrue(
+      slider.hasAction(SwitchAccessMenuAction.INCREMENT),
+      'Slider does not have action INCREMENT');
+  assertTrue(
+      slider.hasAction(SwitchAccessMenuAction.DECREMENT),
+      'Slider does not have action DECREMENT');
 });
diff --git a/chrome/browser/resources/chromeos/accessibility/switch_access/point_scan_manager_test.js b/chrome/browser/resources/chromeos/accessibility/switch_access/point_scan_manager_test.js
index d0aad87..eb34588 100644
--- a/chrome/browser/resources/chromeos/accessibility/switch_access/point_scan_manager_test.js
+++ b/chrome/browser/resources/chromeos/accessibility/switch_access/point_scan_manager_test.js
@@ -23,84 +23,89 @@
   }
 };
 
-TEST_F('SwitchAccessPointScanManagerTest', 'PointScanLeftClick', function() {
-  const website = '<input type=checkbox style="width: 800px; height: 800px;">';
-  this.runWithLoadedTree(website, async (rootWebArea) => {
-    const checkbox = rootWebArea.find({role: 'checkBox'});
-    checkbox.doDefault();
+TEST_F(
+    'SwitchAccessPointScanManagerTest', 'PointScanLeftClick', async function() {
+      const website =
+          '<input type=checkbox style="width: 800px; height: 800px;">';
+      const rootWebArea = await this.runWithLoadedTree(website);
+      const checkbox = rootWebArea.find({role: 'checkBox'});
+      checkbox.doDefault();
 
-    const verifyChecked = checked => resolve => {
-      const checkedHandler = event => {
-        assertEquals(event.target.checked, String(checked));
-        event.target.removeEventListener(
-            chrome.automation.EventType.CHECKED_STATE_CHANGED, checkedHandler);
-        resolve();
-      };
-      checkbox.addEventListener(
-          chrome.automation.EventType.CHECKED_STATE_CHANGED, checkedHandler);
-    };
-    await new Promise(verifyChecked(true));
-
-    SwitchAccess.mode = SAConstants.Mode.POINT_SCAN;
-    Navigator.byPoint.point_ = {x: 600, y: 600};
-    Navigator.byPoint.performMouseAction(SwitchAccessMenuAction.LEFT_CLICK);
-    await new Promise(verifyChecked(false));
-  });
-});
-
-TEST_F('SwitchAccessPointScanManagerTest', 'PointScanRightClick', function() {
-  const website = '<p>Kittens r cute</p>';
-  this.runWithLoadedTree(website, async (rootWebArea) => {
-    const findParams = {role: 'menuItem', attributes: {name: /Back.*/}};
-    // Context menu with back button shouldn't exist yet.
-    const initialMenuItem = rootWebArea.find(findParams);
-    assertEquals(initialMenuItem, null);
-
-    const menuItemLoaded = () => resolve => {
-      const observer = treeChange => {
-        // Wait for the context menu with the back button to show up.
-        const menuItem = treeChange.target.find(findParams);
-        if (menuItem !== null) {
-          chrome.automation.removeTreeChangeObserver(observer);
+      const verifyChecked = checked => resolve => {
+        const checkedHandler = event => {
+          assertEquals(event.target.checked, String(checked));
+          event.target.removeEventListener(
+              chrome.automation.EventType.CHECKED_STATE_CHANGED,
+              checkedHandler);
           resolve();
-        }
+        };
+        checkbox.addEventListener(
+            chrome.automation.EventType.CHECKED_STATE_CHANGED, checkedHandler);
       };
-      chrome.automation.addTreeChangeObserver('allTreeChanges', observer);
-    };
+      await new Promise(verifyChecked(true));
 
-    SwitchAccess.mode = SAConstants.Mode.POINT_SCAN;
-    Navigator.byPoint.point_ = {x: 400, y: 400};
-    Navigator.byPoint.performMouseAction(SwitchAccessMenuAction.RIGHT_CLICK);
-    await new Promise(menuItemLoaded());
-  });
-});
+      SwitchAccess.mode = SAConstants.Mode.POINT_SCAN;
+      Navigator.byPoint.point_ = {x: 600, y: 600};
+      Navigator.byPoint.performMouseAction(SwitchAccessMenuAction.LEFT_CLICK);
+      await new Promise(verifyChecked(false));
+    });
+
+TEST_F(
+    'SwitchAccessPointScanManagerTest', 'PointScanRightClick',
+    async function() {
+      const website = '<p>Kittens r cute</p>';
+      const rootWebArea = await this.runWithLoadedTree(website);
+      const findParams = {role: 'menuItem', attributes: {name: /Back.*/}};
+      // Context menu with back button shouldn't exist yet.
+      const initialMenuItem = rootWebArea.find(findParams);
+      assertEquals(initialMenuItem, null);
+
+      const menuItemLoaded = () => resolve => {
+        const observer = treeChange => {
+          // Wait for the context menu with the back button to show up.
+          const menuItem = treeChange.target.find(findParams);
+          if (menuItem !== null) {
+            chrome.automation.removeTreeChangeObserver(observer);
+            resolve();
+          }
+        };
+        chrome.automation.addTreeChangeObserver('allTreeChanges', observer);
+      };
+
+      SwitchAccess.mode = SAConstants.Mode.POINT_SCAN;
+      Navigator.byPoint.point_ = {x: 400, y: 400};
+      Navigator.byPoint.performMouseAction(SwitchAccessMenuAction.RIGHT_CLICK);
+      await new Promise(menuItemLoaded());
+    });
 
 // Verifies that chrome.accessibilityPrivate.setFocusRings() is not called when
 // point scanning is running.
-TEST_F('SwitchAccessPointScanManagerTest', 'PointScanNoFocusRings', function() {
-  const sleep = () => {
-    return new Promise(resolve => setTimeout(resolve, 2 * 1000));
-  };
+TEST_F(
+    'SwitchAccessPointScanManagerTest', 'PointScanNoFocusRings',
+    async function() {
+      const sleep = () => {
+        return new Promise(resolve => setTimeout(resolve, 2 * 1000));
+      };
 
-  const site = '<button>Test</button>';
-  this.runWithLoadedTree(site, async (rootWebArea) => {
-    let setFocusRingsCallCount = 0;
-    // Mock this API to track how many times it's called.
-    chrome.accessibilityPrivate.setFocusRings = (focusRings) => {
-      setFocusRingsCallCount += 1;
-    };
-    assertEquals(0, setFocusRingsCallCount);
-    Navigator.byPoint.start();
-    // When point scanning starts, setFocusRings() gets called once to clear
-    // the focus rings.
-    assertEquals(1, setFocusRingsCallCount);
-    // Simulate the page focusing the button.
-    const button = rootWebArea.find({role: chrome.automation.RoleType.BUTTON});
-    assertNotNullNorUndefined(button);
-    button.focus();
-    // Allow point scanning to run for 2 seconds and ensure no extra calls to
-    // setFocusRings().
-    await sleep();
-    assertEquals(1, setFocusRingsCallCount);
-  });
-});
+      const site = '<button>Test</button>';
+      const rootWebArea = await this.runWithLoadedTree(site);
+      let setFocusRingsCallCount = 0;
+      // Mock this API to track how many times it's called.
+      chrome.accessibilityPrivate.setFocusRings = (focusRings) => {
+        setFocusRingsCallCount += 1;
+      };
+      assertEquals(0, setFocusRingsCallCount);
+      Navigator.byPoint.start();
+      // When point scanning starts, setFocusRings() gets called once to clear
+      // the focus rings.
+      assertEquals(1, setFocusRingsCallCount);
+      // Simulate the page focusing the button.
+      const button =
+          rootWebArea.find({role: chrome.automation.RoleType.BUTTON});
+      assertNotNullNorUndefined(button);
+      button.focus();
+      // Allow point scanning to run for 2 seconds and ensure no extra calls to
+      // setFocusRings().
+      await sleep();
+      assertEquals(1, setFocusRingsCallCount);
+    });
diff --git a/chrome/browser/resources/chromeos/accessibility/switch_access/switch_access_predicate_test.js b/chrome/browser/resources/chromeos/accessibility/switch_access/switch_access_predicate_test.js
index fa359d6..4567d90 100644
--- a/chrome/browser/resources/chromeos/accessibility/switch_access/switch_access_predicate_test.js
+++ b/chrome/browser/resources/chromeos/accessibility/switch_access/switch_access_predicate_test.js
@@ -89,154 +89,151 @@
   };
 }
 
-TEST_F('SwitchAccessPredicateTest', 'IsInteresting', function() {
-  this.runWithLoadedTree(testWebsite(), (loadedPage) => {
-    const t = getTree(loadedPage);
-    const cache = new SACache();
+TEST_F('SwitchAccessPredicateTest', 'IsInteresting', async function() {
+  const loadedPage = await this.runWithLoadedTree(testWebsite());
+  const t = getTree(loadedPage);
+  const cache = new SACache();
 
-    // The scope is only used to verify the locations are not the same, and
-    // since the buildTree function depends on isInteresting, pass in null
-    // for the scope.
-    assertTrue(
-        SwitchAccessPredicate.isInteresting(t.root, null, cache),
-        'Root should be interesting');
-    assertTrue(
-        SwitchAccessPredicate.isInteresting(t.upper1, null, cache),
-        'Upper1 should be interesting');
-    assertTrue(
-        SwitchAccessPredicate.isInteresting(t.upper2, null, cache),
-        'Upper2 should be interesting');
-    assertTrue(
-        SwitchAccessPredicate.isInteresting(t.lower1, null, cache),
-        'Lower1 should be interesting');
-    assertFalse(
-        SwitchAccessPredicate.isInteresting(t.lower2, null, cache),
-        'Lower2 should not be interesting');
-    assertFalse(
-        SwitchAccessPredicate.isInteresting(t.lower3, null, cache),
-        'Lower3 should not be interesting');
-    assertTrue(
-        SwitchAccessPredicate.isInteresting(t.leaf1, null, cache),
-        'Leaf1 should be interesting');
-    assertFalse(
-        SwitchAccessPredicate.isInteresting(t.leaf2, null, cache),
-        'Leaf2 should not be interesting');
-    assertTrue(
-        SwitchAccessPredicate.isInteresting(t.leaf3, null, cache),
-        'Leaf3 should be interesting');
-    assertFalse(
-        SwitchAccessPredicate.isInteresting(t.leaf4, null, cache),
-        'Leaf4 should not be interesting');
-    assertTrue(
-        SwitchAccessPredicate.isInteresting(t.leaf5, null, cache),
-        'Leaf5 should be interesting');
-    assertFalse(
-        SwitchAccessPredicate.isInteresting(t.leaf6, null, cache),
-        'Leaf6 should not be interesting');
-    assertFalse(
-        SwitchAccessPredicate.isInteresting(t.leaf7, null, cache),
-        'Leaf7 should not be interesting');
-  });
+  // The scope is only used to verify the locations are not the same, and
+  // since the buildTree function depends on isInteresting, pass in null
+  // for the scope.
+  assertTrue(
+      SwitchAccessPredicate.isInteresting(t.root, null, cache),
+      'Root should be interesting');
+  assertTrue(
+      SwitchAccessPredicate.isInteresting(t.upper1, null, cache),
+      'Upper1 should be interesting');
+  assertTrue(
+      SwitchAccessPredicate.isInteresting(t.upper2, null, cache),
+      'Upper2 should be interesting');
+  assertTrue(
+      SwitchAccessPredicate.isInteresting(t.lower1, null, cache),
+      'Lower1 should be interesting');
+  assertFalse(
+      SwitchAccessPredicate.isInteresting(t.lower2, null, cache),
+      'Lower2 should not be interesting');
+  assertFalse(
+      SwitchAccessPredicate.isInteresting(t.lower3, null, cache),
+      'Lower3 should not be interesting');
+  assertTrue(
+      SwitchAccessPredicate.isInteresting(t.leaf1, null, cache),
+      'Leaf1 should be interesting');
+  assertFalse(
+      SwitchAccessPredicate.isInteresting(t.leaf2, null, cache),
+      'Leaf2 should not be interesting');
+  assertTrue(
+      SwitchAccessPredicate.isInteresting(t.leaf3, null, cache),
+      'Leaf3 should be interesting');
+  assertFalse(
+      SwitchAccessPredicate.isInteresting(t.leaf4, null, cache),
+      'Leaf4 should not be interesting');
+  assertTrue(
+      SwitchAccessPredicate.isInteresting(t.leaf5, null, cache),
+      'Leaf5 should be interesting');
+  assertFalse(
+      SwitchAccessPredicate.isInteresting(t.leaf6, null, cache),
+      'Leaf6 should not be interesting');
+  assertFalse(
+      SwitchAccessPredicate.isInteresting(t.leaf7, null, cache),
+      'Leaf7 should not be interesting');
 });
 
-TEST_F('SwitchAccessPredicateTest', 'IsGroup', function() {
-  this.runWithLoadedTree(testWebsite(), (loadedPage) => {
-    const t = getTree(loadedPage);
-    const cache = new SACache();
+TEST_F('SwitchAccessPredicateTest', 'IsGroup', async function() {
+  const loadedPage = await this.runWithLoadedTree(testWebsite());
+  const t = getTree(loadedPage);
+  const cache = new SACache();
 
-    // The scope is only used to verify the locations are not the same, and
-    // since the buildTree function depends on isGroup, pass in null for
-    // the scope.
-    assertTrue(
-        SwitchAccessPredicate.isGroup(t.root, null, cache),
-        'Root should be a group');
-    assertTrue(
-        SwitchAccessPredicate.isGroup(t.upper1, null, cache),
-        'Upper1 should be a group');
-    assertFalse(
-        SwitchAccessPredicate.isGroup(t.upper2, null, cache),
-        'Upper2 should not be a group');
-    assertTrue(
-        SwitchAccessPredicate.isGroup(t.lower1, null, cache),
-        'Lower1 should be a group');
-    assertFalse(
-        SwitchAccessPredicate.isGroup(t.lower2, null, cache),
-        'Lower2 should not be a group');
-    assertFalse(
-        SwitchAccessPredicate.isGroup(t.lower3, null, cache),
-        'Lower3 should not be a group');
-    assertFalse(
-        SwitchAccessPredicate.isGroup(t.leaf1, null, cache),
-        'Leaf1 should not be a group');
-    assertFalse(
-        SwitchAccessPredicate.isGroup(t.leaf2, null, cache),
-        'Leaf2 should not be a group');
-    assertFalse(
-        SwitchAccessPredicate.isGroup(t.leaf3, null, cache),
-        'Leaf3 should not be a group');
-    assertFalse(
-        SwitchAccessPredicate.isGroup(t.leaf4, null, cache),
-        'Leaf4 should not be a group');
-    assertFalse(
-        SwitchAccessPredicate.isGroup(t.leaf5, null, cache),
-        'Leaf5 should not be a group');
-    assertFalse(
-        SwitchAccessPredicate.isGroup(t.leaf6, null, cache),
-        'Leaf6 should not be a group');
-    assertFalse(
-        SwitchAccessPredicate.isGroup(t.leaf7, null, cache),
-        'Leaf7 should not be a group');
-  });
+  // The scope is only used to verify the locations are not the same, and
+  // since the buildTree function depends on isGroup, pass in null for
+  // the scope.
+  assertTrue(
+      SwitchAccessPredicate.isGroup(t.root, null, cache),
+      'Root should be a group');
+  assertTrue(
+      SwitchAccessPredicate.isGroup(t.upper1, null, cache),
+      'Upper1 should be a group');
+  assertFalse(
+      SwitchAccessPredicate.isGroup(t.upper2, null, cache),
+      'Upper2 should not be a group');
+  assertTrue(
+      SwitchAccessPredicate.isGroup(t.lower1, null, cache),
+      'Lower1 should be a group');
+  assertFalse(
+      SwitchAccessPredicate.isGroup(t.lower2, null, cache),
+      'Lower2 should not be a group');
+  assertFalse(
+      SwitchAccessPredicate.isGroup(t.lower3, null, cache),
+      'Lower3 should not be a group');
+  assertFalse(
+      SwitchAccessPredicate.isGroup(t.leaf1, null, cache),
+      'Leaf1 should not be a group');
+  assertFalse(
+      SwitchAccessPredicate.isGroup(t.leaf2, null, cache),
+      'Leaf2 should not be a group');
+  assertFalse(
+      SwitchAccessPredicate.isGroup(t.leaf3, null, cache),
+      'Leaf3 should not be a group');
+  assertFalse(
+      SwitchAccessPredicate.isGroup(t.leaf4, null, cache),
+      'Leaf4 should not be a group');
+  assertFalse(
+      SwitchAccessPredicate.isGroup(t.leaf5, null, cache),
+      'Leaf5 should not be a group');
+  assertFalse(
+      SwitchAccessPredicate.isGroup(t.leaf6, null, cache),
+      'Leaf6 should not be a group');
+  assertFalse(
+      SwitchAccessPredicate.isGroup(t.leaf7, null, cache),
+      'Leaf7 should not be a group');
 });
 
-TEST_F('SwitchAccessPredicateTest', 'IsInterestingSubtree', function() {
-  this.runWithLoadedTree(testWebsite(), (loadedPage) => {
-    const t = getTree(loadedPage);
-    const cache = new SACache();
+TEST_F('SwitchAccessPredicateTest', 'IsInterestingSubtree', async function() {
+  const loadedPage = await this.runWithLoadedTree(testWebsite());
+  const t = getTree(loadedPage);
+  const cache = new SACache();
 
-    assertTrue(
-        SwitchAccessPredicate.isInterestingSubtree(t.root, cache),
-        'Root should be an interesting subtree');
-    assertTrue(
-        SwitchAccessPredicate.isInterestingSubtree(t.upper1, cache),
-        'Upper1 should be an interesting subtree');
-    assertTrue(
-        SwitchAccessPredicate.isInterestingSubtree(t.upper2, cache),
-        'Upper2 should be an interesting subtree');
-    assertTrue(
-        SwitchAccessPredicate.isInterestingSubtree(t.lower1, cache),
-        'Lower1 should be an interesting subtree');
-    assertTrue(
-        SwitchAccessPredicate.isInterestingSubtree(t.lower2, cache),
-        'Lower2 should be an interesting subtree');
-    assertFalse(
-        SwitchAccessPredicate.isInterestingSubtree(t.lower3, cache),
-        'Lower3 should not be an interesting subtree');
-    assertTrue(
-        SwitchAccessPredicate.isInterestingSubtree(t.leaf1, cache),
-        'Leaf1 should be an interesting subtree');
-    assertFalse(
-        SwitchAccessPredicate.isInterestingSubtree(t.leaf2, cache),
-        'Leaf2 should not be an interesting subtree');
-    assertTrue(
-        SwitchAccessPredicate.isInterestingSubtree(t.leaf3, cache),
-        'Leaf3 should be an interesting subtree');
-    assertFalse(
-        SwitchAccessPredicate.isInterestingSubtree(t.leaf4, cache),
-        'Leaf4 should not be an interesting subtree');
-    assertTrue(
-        SwitchAccessPredicate.isInterestingSubtree(t.leaf5, cache),
-        'Leaf5 should be an interesting subtree');
-    assertFalse(
-        SwitchAccessPredicate.isInterestingSubtree(t.leaf6, cache),
-        'Leaf6 should not be an interesting subtree');
-    assertFalse(
-        SwitchAccessPredicate.isInterestingSubtree(t.leaf7, cache),
-        'Leaf7 should not be an interesting subtree');
-  });
+  assertTrue(
+      SwitchAccessPredicate.isInterestingSubtree(t.root, cache),
+      'Root should be an interesting subtree');
+  assertTrue(
+      SwitchAccessPredicate.isInterestingSubtree(t.upper1, cache),
+      'Upper1 should be an interesting subtree');
+  assertTrue(
+      SwitchAccessPredicate.isInterestingSubtree(t.upper2, cache),
+      'Upper2 should be an interesting subtree');
+  assertTrue(
+      SwitchAccessPredicate.isInterestingSubtree(t.lower1, cache),
+      'Lower1 should be an interesting subtree');
+  assertTrue(
+      SwitchAccessPredicate.isInterestingSubtree(t.lower2, cache),
+      'Lower2 should be an interesting subtree');
+  assertFalse(
+      SwitchAccessPredicate.isInterestingSubtree(t.lower3, cache),
+      'Lower3 should not be an interesting subtree');
+  assertTrue(
+      SwitchAccessPredicate.isInterestingSubtree(t.leaf1, cache),
+      'Leaf1 should be an interesting subtree');
+  assertFalse(
+      SwitchAccessPredicate.isInterestingSubtree(t.leaf2, cache),
+      'Leaf2 should not be an interesting subtree');
+  assertTrue(
+      SwitchAccessPredicate.isInterestingSubtree(t.leaf3, cache),
+      'Leaf3 should be an interesting subtree');
+  assertFalse(
+      SwitchAccessPredicate.isInterestingSubtree(t.leaf4, cache),
+      'Leaf4 should not be an interesting subtree');
+  assertTrue(
+      SwitchAccessPredicate.isInterestingSubtree(t.leaf5, cache),
+      'Leaf5 should be an interesting subtree');
+  assertFalse(
+      SwitchAccessPredicate.isInterestingSubtree(t.leaf6, cache),
+      'Leaf6 should not be an interesting subtree');
+  assertFalse(
+      SwitchAccessPredicate.isInterestingSubtree(t.leaf7, cache),
+      'Leaf7 should not be an interesting subtree');
 });
 
-TEST_F('SwitchAccessPredicateTest', 'IsActionable', function() {
+TEST_F('SwitchAccessPredicateTest', 'IsActionable', async function() {
   const treeString =
       `<button style="position:absolute; top:-100px;">offscreen</button>
        <button disabled>disabled</button>
@@ -246,62 +243,63 @@
        <input type="range" aria-label="slider" value=5 min=0 max=10>
        <div id="clickable" role="listitem" onclick="2+2"></div>
        <div id="div1"><p>p1</p></div>`;
-  this.runWithLoadedTree(treeString, (loadedPage) => {
-    const cache = new SACache();
+  await this.runWithLoadedTree(treeString);
+  const cache = new SACache();
 
-    const offscreenButton = this.findNodeByNameAndRole('offscreen', 'button');
-    assertFalse(
-        SwitchAccessPredicate.isActionable(offscreenButton, cache),
-        'Offscreen objects should not be actionable');
+  const offscreenButton = this.findNodeByNameAndRole('offscreen', 'button');
+  assertFalse(
+      SwitchAccessPredicate.isActionable(offscreenButton, cache),
+      'Offscreen objects should not be actionable');
 
-    const disabledButton = this.findNodeByNameAndRole('disabled', 'button');
-    assertFalse(
-        SwitchAccessPredicate.isActionable(disabledButton, cache),
-        'Disabled objects should not be actionable');
+  const disabledButton = this.findNodeByNameAndRole('disabled', 'button');
+  assertFalse(
+      SwitchAccessPredicate.isActionable(disabledButton, cache),
+      'Disabled objects should not be actionable');
 
-    assertFalse(
-        SwitchAccessPredicate.isActionable(loadedPage, cache),
-        'Root web area should not be directly actionable');
+  assertFalse(
+      SwitchAccessPredicate.isActionable(loadedPage, cache),
+      'Root web area should not be directly actionable');
 
-    const link1 = this.findNodeByNameAndRole('link1', 'link');
-    assertTrue(
-        SwitchAccessPredicate.isActionable(link1, cache),
-        'Links should be actionable');
+  const link1 = this.findNodeByNameAndRole('link1', 'link');
+  assertTrue(
+      SwitchAccessPredicate.isActionable(link1, cache),
+      'Links should be actionable');
 
-    const input1 = this.findNodeByNameAndRole('input1', 'textField');
-    assertTrue(
-        SwitchAccessPredicate.isActionable(input1, cache),
-        'Inputs should be actionable');
+  const input1 = this.findNodeByNameAndRole('input1', 'textField');
+  assertTrue(
+      SwitchAccessPredicate.isActionable(input1, cache),
+      'Inputs should be actionable');
 
-    const button3 = this.findNodeByNameAndRole('button3', 'button');
-    assertTrue(
-        SwitchAccessPredicate.isActionable(button3, cache),
-        'Buttons should be actionable');
+  const button3 = this.findNodeByNameAndRole('button3', 'button');
+  assertTrue(
+      SwitchAccessPredicate.isActionable(button3, cache),
+      'Buttons should be actionable');
 
-    const slider = this.findNodeByNameAndRole('slider', 'slider');
-    assertTrue(
-        SwitchAccessPredicate.isActionable(slider, cache),
-        'Sliders should be actionable');
+  const slider = this.findNodeByNameAndRole('slider', 'slider');
+  assertTrue(
+      SwitchAccessPredicate.isActionable(slider, cache),
+      'Sliders should be actionable');
 
-    const clickable = this.findNodeById('clickable');
-    assertTrue(
-        SwitchAccessPredicate.isActionable(clickable, cache),
-        'Clickable list items should be actionable');
+  const clickable = this.findNodeById('clickable');
+  assertTrue(
+      SwitchAccessPredicate.isActionable(clickable, cache),
+      'Clickable list items should be actionable');
 
-    const div1 = this.findNodeById('div1');
-    assertFalse(
-        SwitchAccessPredicate.isActionable(div1, cache),
-        'Divs should not generally be actionable');
+  const div1 = this.findNodeById('div1');
+  assertFalse(
+      SwitchAccessPredicate.isActionable(div1, cache),
+      'Divs should not generally be actionable');
 
-    const p1 = this.findNodeByNameAndRole('p1', 'staticText');
-    assertFalse(
-        SwitchAccessPredicate.isActionable(p1, cache),
-        'Static text should not generally be actionable');
-  });
+  const p1 = this.findNodeByNameAndRole('p1', 'staticText');
+  assertFalse(
+      SwitchAccessPredicate.isActionable(p1, cache),
+      'Static text should not generally be actionable');
 });
 
-TEST_F('SwitchAccessPredicateTest', 'IsActionableFocusableElements', function() {
-  const treeString = `<div id="noChildren" tabindex=0></div>
+TEST_F(
+    'SwitchAccessPredicateTest', 'IsActionableFocusableElements',
+    async function() {
+      const treeString = `<div id="noChildren" tabindex=0></div>
        <div id="oneInterestingChild" tabindex=0>
          <div>
            <div>
@@ -322,163 +320,158 @@
          <p>p2</p>
          <p>p3</p>
        </div>`;
-  this.runWithLoadedTree(treeString, (loadedPage) => {
-    const cache = new SACache();
+      await this.runWithLoadedTree(treeString);
+      const cache = new SACache();
 
-    const noChildren = this.findNodeById('noChildren');
-    assertTrue(
-        SwitchAccessPredicate.isActionable(noChildren, cache),
-        'Focusable element with no children should be actionable');
+      const noChildren = this.findNodeById('noChildren');
+      assertTrue(
+          SwitchAccessPredicate.isActionable(noChildren, cache),
+          'Focusable element with no children should be actionable');
 
-    const oneInterestingChild = this.findNodeById('oneInterestingChild');
-    assertFalse(
-        SwitchAccessPredicate.isActionable(oneInterestingChild, cache),
-        'Focusable element with an interesting child should not be actionable');
+      const oneInterestingChild = this.findNodeById('oneInterestingChild');
+      assertFalse(
+          SwitchAccessPredicate.isActionable(oneInterestingChild, cache),
+          'Focusable element with an interesting child should not be actionable');
 
-    const interestingChildren = this.findNodeById('interestingChildren');
-    assertFalse(
-        SwitchAccessPredicate.isActionable(interestingChildren, cache),
-        'Focusable element with interesting children should not be actionable');
+      const interestingChildren = this.findNodeById('interestingChildren');
+      assertFalse(
+          SwitchAccessPredicate.isActionable(interestingChildren, cache),
+          'Focusable element with interesting children should not be ' +
+              'actionable');
 
-    const oneUninterestingChild = this.findNodeById('oneUninterestingChild');
-    assertTrue(
-        SwitchAccessPredicate.isActionable(oneUninterestingChild, cache),
-        'Focusable element with one uninteresting child should be actionable');
+      const oneUninterestingChild = this.findNodeById('oneUninterestingChild');
+      assertTrue(
+          SwitchAccessPredicate.isActionable(oneUninterestingChild, cache),
+          'Focusable element with one uninteresting child should be ' +
+              'actionable');
 
-    const uninterestingChildren = this.findNodeById('uninterestingChildren');
-    assertTrue(
-        SwitchAccessPredicate.isActionable(uninterestingChildren, cache),
-        'Focusable element with uninteresting children should be actionable');
-  });
+      const uninterestingChildren = this.findNodeById('uninterestingChildren');
+      assertTrue(
+          SwitchAccessPredicate.isActionable(uninterestingChildren, cache),
+          'Focusable element with uninteresting children should be actionable');
+    });
+
+TEST_F('SwitchAccessPredicateTest', 'LeafPredicate', async function() {
+  const loadedPage = await this.runWithLoadedTree(testWebsite());
+  const t = getTree(loadedPage);
+  const cache = new SACache();
+
+  // Start with root as scope
+  let leaf = SwitchAccessPredicate.leaf(t.root, cache);
+  assertFalse(leaf(t.root), 'Root should not be a leaf node');
+  assertTrue(leaf(t.upper1), 'Upper1 should be a leaf node for root tree');
+  assertTrue(leaf(t.upper2), 'Upper2 should be a leaf node for root tree');
+
+  // Set upper1 as scope
+  leaf = SwitchAccessPredicate.leaf(t.upper1, cache);
+  assertFalse(leaf(t.upper1), 'Upper1 should not be a leaf for upper1 tree');
+  assertTrue(leaf(t.lower1), 'Lower1 should be a leaf for upper1 tree');
+  assertTrue(leaf(t.leaf4), 'leaf4 should be a leaf for upper1 tree');
+  assertTrue(leaf(t.leaf5), 'leaf5 should be a leaf for upper1 tree');
+
+  // Set lower1 as scope
+  leaf = SwitchAccessPredicate.leaf(t.lower1, cache);
+  assertFalse(leaf(t.lower1), 'Lower1 should not be a leaf for lower1 tree');
+  assertTrue(leaf(t.leaf1), 'Leaf1 should be a leaf for lower1 tree');
+  assertTrue(leaf(t.leaf2), 'Leaf2 should be a leaf for lower1 tree');
+  assertTrue(leaf(t.leaf3), 'Leaf3 should be a leaf for lower1 tree');
 });
 
-TEST_F('SwitchAccessPredicateTest', 'LeafPredicate', function() {
-  this.runWithLoadedTree(testWebsite(), (loadedPage) => {
-    const t = getTree(loadedPage);
-    const cache = new SACache();
+TEST_F('SwitchAccessPredicateTest', 'RootPredicate', async function() {
+  const loadedPage = await this.runWithLoadedTree(testWebsite());
+  const t = getTree(loadedPage);
 
-    // Start with root as scope
-    let leaf = SwitchAccessPredicate.leaf(t.root, cache);
-    assertFalse(leaf(t.root), 'Root should not be a leaf node');
-    assertTrue(leaf(t.upper1), 'Upper1 should be a leaf node for root tree');
-    assertTrue(leaf(t.upper2), 'Upper2 should be a leaf node for root tree');
+  // Start with root as scope
+  let root = SwitchAccessPredicate.root(t.root);
+  assertTrue(root(t.root), 'Root should be a root of the root tree');
+  assertFalse(root(t.upper1), 'Upper1 should not be a root of the root tree');
+  assertFalse(root(t.upper2), 'Upper2 should not be a root of the root tree');
 
-    // Set upper1 as scope
-    leaf = SwitchAccessPredicate.leaf(t.upper1, cache);
-    assertFalse(leaf(t.upper1), 'Upper1 should not be a leaf for upper1 tree');
-    assertTrue(leaf(t.lower1), 'Lower1 should be a leaf for upper1 tree');
-    assertTrue(leaf(t.leaf4), 'leaf4 should be a leaf for upper1 tree');
-    assertTrue(leaf(t.leaf5), 'leaf5 should be a leaf for upper1 tree');
+  // Set upper1 as scope
+  root = SwitchAccessPredicate.root(t.upper1);
+  assertTrue(root(t.upper1), 'Upper1 should be a root of the upper1 tree');
+  assertFalse(root(t.lower1), 'Lower1 should not be a root of the upper1 tree');
+  assertFalse(root(t.lower2), 'Lower2 should not be a root of the upper1 tree');
 
-    // Set lower1 as scope
-    leaf = SwitchAccessPredicate.leaf(t.lower1, cache);
-    assertFalse(leaf(t.lower1), 'Lower1 should not be a leaf for lower1 tree');
-    assertTrue(leaf(t.leaf1), 'Leaf1 should be a leaf for lower1 tree');
-    assertTrue(leaf(t.leaf2), 'Leaf2 should be a leaf for lower1 tree');
-    assertTrue(leaf(t.leaf3), 'Leaf3 should be a leaf for lower1 tree');
-  });
+  // Set lower1 as scope
+  root = SwitchAccessPredicate.root(t.lower1);
+  assertTrue(root(t.lower1), 'Lower1 should be a root of the lower1 tree');
+  assertFalse(root(t.leaf1), 'Leaf1 should not be a root of the lower1 tree');
+  assertFalse(root(t.leaf2), 'Leaf2 should not be a root of the lower1 tree');
+  assertFalse(root(t.leaf3), 'Leaf3 should not be a root of the lower1 tree');
 });
 
-TEST_F('SwitchAccessPredicateTest', 'RootPredicate', function() {
-  this.runWithLoadedTree(testWebsite(), (loadedPage) => {
-    const t = getTree(loadedPage);
+TEST_F('SwitchAccessPredicateTest', 'VisitPredicate', async function() {
+  const loadedPage = await this.runWithLoadedTree(testWebsite());
+  const t = getTree(loadedPage);
+  const cache = new SACache();
 
-    // Start with root as scope
-    let root = SwitchAccessPredicate.root(t.root);
-    assertTrue(root(t.root), 'Root should be a root of the root tree');
-    assertFalse(root(t.upper1), 'Upper1 should not be a root of the root tree');
-    assertFalse(root(t.upper2), 'Upper2 should not be a root of the root tree');
+  // Start with root as scope
+  let visit = SwitchAccessPredicate.visit(t.root, cache);
+  assertTrue(visit(t.root), 'Root should be visited in root tree');
+  assertTrue(visit(t.upper1), 'Upper1 should be visited in root tree');
+  assertTrue(visit(t.upper2), 'Upper2 should be visited in root tree');
 
-    // Set upper1 as scope
-    root = SwitchAccessPredicate.root(t.upper1);
-    assertTrue(root(t.upper1), 'Upper1 should be a root of the upper1 tree');
-    assertFalse(
-        root(t.lower1), 'Lower1 should not be a root of the upper1 tree');
-    assertFalse(
-        root(t.lower2), 'Lower2 should not be a root of the upper1 tree');
+  // Set upper1 as scope
+  visit = SwitchAccessPredicate.visit(t.upper1, cache);
+  assertTrue(visit(t.upper1), 'Upper1 should be visited in upper1 tree');
+  assertTrue(visit(t.lower1), 'Lower1 should be visited in upper1 tree');
+  assertFalse(visit(t.lower2), 'Lower2 should not be visited in upper1 tree');
+  assertFalse(visit(t.leaf4), 'Leaf4 should not be visited in upper1 tree');
+  assertTrue(visit(t.leaf5), 'Leaf5 should be visited in upper1 tree');
 
-    // Set lower1 as scope
-    root = SwitchAccessPredicate.root(t.lower1);
-    assertTrue(root(t.lower1), 'Lower1 should be a root of the lower1 tree');
-    assertFalse(root(t.leaf1), 'Leaf1 should not be a root of the lower1 tree');
-    assertFalse(root(t.leaf2), 'Leaf2 should not be a root of the lower1 tree');
-    assertFalse(root(t.leaf3), 'Leaf3 should not be a root of the lower1 tree');
-  });
+  // Set lower1 as scope
+  visit = SwitchAccessPredicate.visit(t.lower1, cache);
+  assertTrue(visit(t.lower1), 'Lower1 should be visited in lower1 tree');
+  assertTrue(visit(t.leaf1), 'Leaf1 should be visited in lower1 tree');
+  assertFalse(visit(t.leaf2), 'Leaf2 should not be visited in lower1 tree');
+  assertTrue(visit(t.leaf3), 'Leaf3 should be visited in lower1 tree');
+
+  // An uninteresting subtree should return false, regardless of scope
+  assertFalse(visit(t.lower3), 'Lower3 should not be visited in lower1 tree');
+  assertFalse(visit(t.leaf6), 'Leaf6 should not be visited in lower1 tree');
+  assertFalse(visit(t.leaf7), 'Leaf7 should not be visited in lower1 tree');
 });
 
-TEST_F('SwitchAccessPredicateTest', 'VisitPredicate', function() {
-  this.runWithLoadedTree(testWebsite(), (loadedPage) => {
-    const t = getTree(loadedPage);
-    const cache = new SACache();
+TEST_F('SwitchAccessPredicateTest', 'Cache', async function() {
+  const loadedPage = await this.runWithLoadedTree(testWebsite());
+  const t = getTree(loadedPage);
+  const cache = new SACache();
 
-    // Start with root as scope
-    let visit = SwitchAccessPredicate.visit(t.root, cache);
-    assertTrue(visit(t.root), 'Root should be visited in root tree');
-    assertTrue(visit(t.upper1), 'Upper1 should be visited in root tree');
-    assertTrue(visit(t.upper2), 'Upper2 should be visited in root tree');
-
-    // Set upper1 as scope
-    visit = SwitchAccessPredicate.visit(t.upper1, cache);
-    assertTrue(visit(t.upper1), 'Upper1 should be visited in upper1 tree');
-    assertTrue(visit(t.lower1), 'Lower1 should be visited in upper1 tree');
-    assertFalse(visit(t.lower2), 'Lower2 should not be visited in upper1 tree');
-    assertFalse(visit(t.leaf4), 'Leaf4 should not be visited in upper1 tree');
-    assertTrue(visit(t.leaf5), 'Leaf5 should be visited in upper1 tree');
-
-    // Set lower1 as scope
-    visit = SwitchAccessPredicate.visit(t.lower1, cache);
-    assertTrue(visit(t.lower1), 'Lower1 should be visited in lower1 tree');
-    assertTrue(visit(t.leaf1), 'Leaf1 should be visited in lower1 tree');
-    assertFalse(visit(t.leaf2), 'Leaf2 should not be visited in lower1 tree');
-    assertTrue(visit(t.leaf3), 'Leaf3 should be visited in lower1 tree');
-
-    // An uninteresting subtree should return false, regardless of scope
-    assertFalse(visit(t.lower3), 'Lower3 should not be visited in lower1 tree');
-    assertFalse(visit(t.leaf6), 'Leaf6 should not be visited in lower1 tree');
-    assertFalse(visit(t.leaf7), 'Leaf7 should not be visited in lower1 tree');
-  });
-});
-
-TEST_F('SwitchAccessPredicateTest', 'Cache', function() {
-  this.runWithLoadedTree(testWebsite(), (loadedPage) => {
-    const t = getTree(loadedPage);
-    const cache = new SACache();
-
-    let locationAccessCount = 0;
-    class TestRoot extends SARootNode {
-      /** @override */
-      get location() {
-        locationAccessCount++;
-        return null;
-      }
+  let locationAccessCount = 0;
+  class TestRoot extends SARootNode {
+    /** @override */
+    get location() {
+      locationAccessCount++;
+      return null;
     }
-    const group = new TestRoot(t.root);
+  }
+  const group = new TestRoot(t.root);
 
-    assertTrue(
-        SwitchAccessPredicate.isGroup(t.root, group, cache),
-        'Root should be a group');
-    assertEquals(
-        locationAccessCount, 1,
-        'Location should have been accessed to calculate isGroup');
-    assertTrue(
-        SwitchAccessPredicate.isGroup(t.root, group, cache),
-        'isGroup value should not change');
-    assertEquals(
-        locationAccessCount, 1,
-        'Cache should have been used, avoiding second location access');
+  assertTrue(
+      SwitchAccessPredicate.isGroup(t.root, group, cache),
+      'Root should be a group');
+  assertEquals(
+      locationAccessCount, 1,
+      'Location should have been accessed to calculate isGroup');
+  assertTrue(
+      SwitchAccessPredicate.isGroup(t.root, group, cache),
+      'isGroup value should not change');
+  assertEquals(
+      locationAccessCount, 1,
+      'Cache should have been used, avoiding second location access');
 
-    locationAccessCount = 0;
-    assertFalse(
-        SwitchAccessPredicate.isGroup(t.leaf1, group, cache),
-        'Leaf should not be a group');
-    assertEquals(
-        locationAccessCount, 1,
-        'Location should have been accessed to calculate isGroup');
-    assertFalse(
-        SwitchAccessPredicate.isGroup(t.leaf1, group, cache),
-        'isGroup value should not change');
-    assertEquals(
-        locationAccessCount, 1,
-        'Cache should have been used, avoiding second location access');
-  });
+  locationAccessCount = 0;
+  assertFalse(
+      SwitchAccessPredicate.isGroup(t.leaf1, group, cache),
+      'Leaf should not be a group');
+  assertEquals(
+      locationAccessCount, 1,
+      'Location should have been accessed to calculate isGroup');
+  assertFalse(
+      SwitchAccessPredicate.isGroup(t.leaf1, group, cache),
+      'isGroup value should not change');
+  assertEquals(
+      locationAccessCount, 1,
+      'Cache should have been used, avoiding second location access');
 });
diff --git a/chrome/browser/resources/chromeos/accessibility/switch_access/test_utility.js b/chrome/browser/resources/chromeos/accessibility/switch_access/test_utility.js
index dc8a8ee9..cc9aa05 100644
--- a/chrome/browser/resources/chromeos/accessibility/switch_access/test_utility.js
+++ b/chrome/browser/resources/chromeos/accessibility/switch_access/test_utility.js
@@ -55,7 +55,7 @@
         chrome.accessibilityPrivate.SwitchAccessCommand.SELECT);
   },
 
-  /** Only call from inside runWithLoadedTree() */
+  /** Only call after runWithLoadedTree() */
   startFocusInside(rootWebArea) {
     if (!rootWebArea) {
       throw new Error('Web root node is undefined');
diff --git a/chrome/browser/resources/chromeos/accessibility/switch_access/text_navigation_manager_test.js b/chrome/browser/resources/chromeos/accessibility/switch_access/text_navigation_manager_test.js
index 23d35a0..a11625ef 100644
--- a/chrome/browser/resources/chromeos/accessibility/switch_access/text_navigation_manager_test.js
+++ b/chrome/browser/resources/chromeos/accessibility/switch_access/text_navigation_manager_test.js
@@ -23,7 +23,7 @@
  * executes the specified text navigation action. Upon detecting the
  * text navigation action, the node will verify that the action correctly
  * changed the index of the text caret.
- * @param {!SwitchAccessE2ETest} testHelper
+ * @param {!SwitchAccessE2ETest} testFixture
  * @param {{content: string,
  *          initialIndex: number,
  *          targetIndex: number,
@@ -32,7 +32,7 @@
  *          cols: (number || undefined),
  *          wrap: (string || undefined)}} textParams
  */
-function runTextNavigationTest(testHelper, textParams) {
+async function runTextNavigationTest(testFixture, textParams) {
   // Required parameters.
   const textContent = textParams.content;
   const initialTextIndex = textParams.initialIndex;
@@ -47,16 +47,15 @@
   const website = generateWebsiteWithTextArea(
       textId, textContent, initialTextIndex, textCols, textWrap);
 
-  testHelper.runWithLoadedTree(website, function(rootWebArea) {
-    const inputNode = this.findNodeById(textId);
-    assertNotEquals(inputNode, null);
+  await testFixture.runWithLoadedTree(website);
+  const inputNode = this.findNodeById(textId);
+  assertNotEquals(inputNode, null);
 
-    setUpCursorChangeListener(
-        testHelper, inputNode, initialTextIndex, targetTextIndex,
-        targetTextIndex);
+  setUpCursorChangeListener(
+      testFixture, inputNode, initialTextIndex, targetTextIndex,
+      targetTextIndex);
 
-    textNavigationAction();
-  });
+  textNavigationAction();
 }
 
 /**
@@ -81,10 +80,10 @@
  * in the text area (optional). -wrap: the wrap attribute ("hard" or "soft") of
  * the text area (optional).
  *
- * @param {!SwitchAccessE2ETest} testHelper
+ * @param {!SwitchAccessE2ETest} testFixture
  * @param {selectionTextParams} textParams,
  */
-function runTextSelectionTest(testHelper, textParams) {
+async function runTextSelectionTest(testFixture, textParams) {
   // Required parameters.
   const textContent = textParams.content;
   const initialTextIndex = textParams.initialIndex;
@@ -106,25 +105,24 @@
     navigationTargetIndex = targetTextStartIndex;
   }
 
-  testHelper.runWithLoadedTree(website, function(rootWebArea) {
-    const inputNode = this.findNodeById(textId);
-    assertNotEquals(inputNode, null);
-    checkNodeIsFocused(inputNode);
-    const callback = testHelper.newCallback(function() {
-      setUpCursorChangeListener(
-          testHelper, inputNode, targetTextEndIndex, targetTextStartIndex,
-          targetTextEndIndex);
-      testHelper.textNavigationManager.saveSelectEnd();
-    });
-
-    testHelper.textNavigationManager.saveSelectStart();
-
+  await testFixture.runWithLoadedTree(website);
+  const inputNode = this.findNodeById(textId);
+  assertNotEquals(inputNode, null);
+  checkNodeIsFocused(inputNode);
+  const callback = testFixture.newCallback(function() {
     setUpCursorChangeListener(
-        testHelper, inputNode, initialTextIndex, navigationTargetIndex,
-        navigationTargetIndex, callback);
-
-    textNavigationAction();
+        testFixture, inputNode, targetTextEndIndex, targetTextStartIndex,
+        targetTextEndIndex);
+    testFixture.textNavigationManager.saveSelectEnd();
   });
+
+  testFixture.textNavigationManager.saveSelectStart();
+
+  setUpCursorChangeListener(
+      testFixture, inputNode, initialTextIndex, navigationTargetIndex,
+      navigationTargetIndex, callback);
+
+  textNavigationAction();
 }
 
 /**
@@ -170,7 +168,7 @@
  * change from the text navigation action). Also assumes that
  * the text navigation and selection actions directly changes the text caret
  * to the correct index (with no intermediate movements).
- * @param {!SwitchAccessE2ETest} testHelper
+ * @param {!SwitchAccessE2ETest} testFixture
  * @param {!AutomationNode} inputNode
  * @param {number} initialTextIndex
  * @param {number} targetTextStartIndex
@@ -178,7 +176,7 @@
  * @param {function() || undefined} callback
  */
 function setUpCursorChangeListener(
-    testHelper, inputNode, initialTextIndex, targetTextStartIndex,
+    testFixture, inputNode, initialTextIndex, targetTextStartIndex,
     targetTextEndIndex, callback) {
   // Ensures that the text index has changed before checking the new index.
   const checkActionFinished = function(tab) {
@@ -192,7 +190,7 @@
   };
 
   // Test will not exit until this check is called.
-  const checkTextIndex = testHelper.newCallback(function() {
+  const checkTextIndex = testFixture.newCallback(function() {
     assertEquals(inputNode.textSelStart, targetTextStartIndex);
     assertEquals(inputNode.textSelEnd, targetTextEndIndex);
     // If there's a callback then this is the navigation listener for a
@@ -212,8 +210,8 @@
 // TODO(crbug.com/1268230): Re-enable test.
 TEST_F(
     'SwitchAccessTextNavigationManagerTest', 'DISABLED_JumpToBeginning',
-    function() {
-      runTextNavigationTest(this, {
+    async function() {
+      await runTextNavigationTest(this, {
         content: 'hi there',
         initialIndex: 6,
         targetIndex: 0,
@@ -225,8 +223,9 @@
 
 // TODO(crbug.com/1268230): Re-enable test.
 TEST_F(
-    'SwitchAccessTextNavigationManagerTest', 'DISABLED_JumpToEnd', function() {
-      runTextNavigationTest(this, {
+    'SwitchAccessTextNavigationManagerTest', 'DISABLED_JumpToEnd',
+    async function() {
+      await runTextNavigationTest(this, {
         content: 'hi there',
         initialIndex: 3,
         targetIndex: 8,
@@ -239,8 +238,8 @@
 // TODO(crbug.com/1177096) Renable test
 TEST_F(
     'SwitchAccessTextNavigationManagerTest', 'DISABLED_MoveBackwardOneChar',
-    function() {
-      runTextNavigationTest(this, {
+    async function() {
+      await runTextNavigationTest(this, {
         content: 'parrots!',
         initialIndex: 7,
         targetIndex: 6,
@@ -253,8 +252,8 @@
 // TODO(crbug.com/1268230): Re-enable test.
 TEST_F(
     'SwitchAccessTextNavigationManagerTest', 'DISABLED_MoveBackwardOneWord',
-    function() {
-      runTextNavigationTest(this, {
+    async function() {
+      await runTextNavigationTest(this, {
         content: 'more parrots!',
         initialIndex: 5,
         targetIndex: 0,
@@ -267,8 +266,8 @@
 // TODO(crbug.com/1268230): Re-enable test.
 TEST_F(
     'SwitchAccessTextNavigationManagerTest', 'DISABLED_MoveForwardOneChar',
-    function() {
-      runTextNavigationTest(this, {
+    async function() {
+      await runTextNavigationTest(this, {
         content: 'hello',
         initialIndex: 0,
         targetIndex: 1,
@@ -281,8 +280,8 @@
 // TODO(crbug.com/1268230): Re-enable test.
 TEST_F(
     'SwitchAccessTextNavigationManagerTest', 'DISABLED_MoveForwardOneWord',
-    function() {
-      runTextNavigationTest(this, {
+    async function() {
+      await runTextNavigationTest(this, {
         content: 'more parrots!',
         initialIndex: 4,
         targetIndex: 12,
@@ -295,8 +294,8 @@
 // TODO(crbug.com/1268230): Re-enable test.
 TEST_F(
     'SwitchAccessTextNavigationManagerTest', 'DISABLED_MoveUpOneLine',
-    function() {
-      runTextNavigationTest(this, {
+    async function() {
+      await runTextNavigationTest(this, {
         content: 'more parrots!',
         initialIndex: 7,
         targetIndex: 2,
@@ -311,8 +310,8 @@
 // TODO(crbug.com/1268230): Re-enable test.
 TEST_F(
     'SwitchAccessTextNavigationManagerTest', 'DISABLED_MoveDownOneLine',
-    function() {
-      runTextNavigationTest(this, {
+    async function() {
+      await runTextNavigationTest(this, {
         content: 'more parrots!',
         initialIndex: 3,
         targetIndex: 8,
@@ -331,19 +330,18 @@
  */
 TEST_F(
     'SwitchAccessTextNavigationManagerTest', 'DISABLED_SelectStart',
-    function() {
+    async function() {
       const website =
           generateWebsiteWithTextArea('test', 'test123', 3, 20, 'hard');
 
-      this.runWithLoadedTree(website, function(rootWebArea) {
-        const inputNode = this.findNodeById('test');
-        assertNotEquals(inputNode, null);
-        checkNodeIsFocused(inputNode);
+      await this.runWithLoadedTree(website);
+      const inputNode = this.findNodeById('test');
+      assertNotEquals(inputNode, null);
+      checkNodeIsFocused(inputNode);
 
-        this.textNavigationManager.saveSelectStart();
-        const startIndex = this.textNavigationManager.selectionStartIndex_;
-        assertEquals(startIndex, 3);
-      });
+      this.textNavigationManager.saveSelectStart();
+      const startIndex = this.textNavigationManager.selectionStartIndex_;
+      assertEquals(startIndex, 3);
     });
 
 /**
@@ -352,23 +350,23 @@
  * bounds
  */
 TEST_F(
-    'SwitchAccessTextNavigationManagerTest', 'DISABLED_SelectEnd', function() {
+    'SwitchAccessTextNavigationManagerTest', 'DISABLED_SelectEnd',
+    async function() {
       const website =
           generateWebsiteWithTextArea('test', 'test 123', 6, 20, 'hard');
 
-      this.runWithLoadedTree(website, function(rootWebArea) {
-        const inputNode = this.findNodeById('test');
-        assertNotEquals(inputNode, null);
-        checkNodeIsFocused(inputNode);
+      await this.runWithLoadedTree(website);
+      const inputNode = this.findNodeById('test');
+      assertNotEquals(inputNode, null);
+      checkNodeIsFocused(inputNode);
 
 
-        const startIndex = 3;
-        this.textNavigationManager.selectionStartIndex_ = startIndex;
-        this.textNavigationManager.selectionStartObject_ = inputNode;
-        this.textNavigationManager.saveSelectEnd();
-        const endIndex = inputNode.textSelEnd;
-        assertEquals(6, endIndex);
-      });
+      const startIndex = 3;
+      this.textNavigationManager.selectionStartIndex_ = startIndex;
+      this.textNavigationManager.selectionStartObject_ = inputNode;
+      this.textNavigationManager.saveSelectEnd();
+      const endIndex = inputNode.textSelEnd;
+      assertEquals(6, endIndex);
     });
 
 /**
@@ -377,8 +375,8 @@
  */
 TEST_F(
     'SwitchAccessTextNavigationManagerTest', 'DISABLED_SelectCharacter',
-    function() {
-      runTextSelectionTest(this, {
+    async function() {
+      await runTextSelectionTest(this, {
         content: 'hello world!',
         initialIndex: 0,
         targetStartIndex: 0,
@@ -397,8 +395,8 @@
  */
 TEST_F(
     'SwitchAccessTextNavigationManagerTest', 'DISABLED_SelectWordBackward',
-    function() {
-      runTextSelectionTest(this, {
+    async function() {
+      await runTextSelectionTest(this, {
         content: 'hello world!',
         initialIndex: 5,
         targetStartIndex: 0,
diff --git a/chrome/browser/resources/settings/chromeos/multidevice_page/multidevice_browser_proxy.js b/chrome/browser/resources/settings/chromeos/multidevice_page/multidevice_browser_proxy.js
index 03ce440..e434508b 100644
--- a/chrome/browser/resources/settings/chromeos/multidevice_page/multidevice_browser_proxy.js
+++ b/chrome/browser/resources/settings/chromeos/multidevice_page/multidevice_browser_proxy.js
@@ -92,6 +92,16 @@
    * Cancels the phone hub apps access setup flow.
    */
   cancelAppsSetup() {}
+
+  /**
+   * Attempts the phone hub combined feature access setup flow.
+   */
+  attemptCombinedFeatureSetup(cameraRoll, notifications) {}
+
+  /**
+   * Cancels the phone hub combined feature access setup flow.
+   */
+  cancelCombinedFeatureSetup() {}
 }
 
 /**
@@ -168,6 +178,16 @@
   cancelAppsSetup() {
     chrome.send('cancelAppsSetup');
   }
+
+  /** @override */
+  attemptCombinedFeatureSetup(cameraRoll, notifications) {
+    chrome.send('attemptCombinedFeatureSetup', [cameraRoll, notifications]);
+  }
+
+  /** @override */
+  cancelCombinedFeatureSetup() {
+    chrome.send('cancelCombinedFeatureSetup');
+  }
 }
 
 addSingletonGetter(MultiDeviceBrowserProxyImpl);
diff --git a/chrome/browser/resources/settings/chromeos/multidevice_page/multidevice_constants.js b/chrome/browser/resources/settings/chromeos/multidevice_page/multidevice_constants.js
index 625e6ac..6999cbe 100644
--- a/chrome/browser/resources/settings/chromeos/multidevice_page/multidevice_constants.js
+++ b/chrome/browser/resources/settings/chromeos/multidevice_page/multidevice_constants.js
@@ -141,7 +141,8 @@
  *   isNearbyShareDisallowedByPolicy: boolean,
  *   isPhoneHubAppsAccessGranted: boolean,
  *   isPhoneHubPermissionsDialogSupported: boolean,
- *   isCameraRollFilePermissionGranted: boolean
+ *   isCameraRollFilePermissionGranted: boolean,
+ *   isPhoneHubFeatureCombinedSetupSupported: boolean
  * }}
  */
 export let MultiDevicePageContentData;
diff --git a/chrome/browser/resources/settings/chromeos/multidevice_page/multidevice_page.html b/chrome/browser/resources/settings/chromeos/multidevice_page/multidevice_page.html
index 96c37f5..32dbd4b 100644
--- a/chrome/browser/resources/settings/chromeos/multidevice_page/multidevice_page.html
+++ b/chrome/browser/resources/settings/chromeos/multidevice_page/multidevice_page.html
@@ -210,6 +210,8 @@
                                           pageContentData)]]"
       show-app-streaming="[[isPhoneHubAppsSetupRequired(
                                           pageContentData)]]"
+      combined-setup-supported="[[isCombinedSetupSupported_(
+                                          pageContentData)]]"
       on-close="onHidePhonePermissionsSetupDialog_">
   </settings-multidevice-permissions-setup-dialog>
 </template>
diff --git a/chrome/browser/resources/settings/chromeos/multidevice_page/multidevice_page.js b/chrome/browser/resources/settings/chromeos/multidevice_page/multidevice_page.js
index 7de1412..03197644 100644
--- a/chrome/browser/resources/settings/chromeos/multidevice_page/multidevice_page.js
+++ b/chrome/browser/resources/settings/chromeos/multidevice_page/multidevice_page.js
@@ -692,4 +692,14 @@
     return loadTimeData.getBoolean('isNearbyShareBackgroundScanningEnabled') &&
         is_hardware_supported;
   },
+
+  /**
+   * Whether the combined setup for Notifications and Camera Roll is supported
+   * on the connected phone.
+   * @return {boolean}
+   * @private
+   */
+  isCombinedSetupSupported_() {
+    return this.pageContentData.isPhoneHubFeatureCombinedSetupSupported;
+  },
 });
diff --git a/chrome/browser/resources/settings/chromeos/multidevice_page/multidevice_permissions_setup_dialog.js b/chrome/browser/resources/settings/chromeos/multidevice_page/multidevice_permissions_setup_dialog.js
index 01ab2f93..207de80 100644
--- a/chrome/browser/resources/settings/chromeos/multidevice_page/multidevice_permissions_setup_dialog.js
+++ b/chrome/browser/resources/settings/chromeos/multidevice_page/multidevice_permissions_setup_dialog.js
@@ -52,6 +52,7 @@
   SET_LOCKSCREEN: 1,
   WAIT_FOR_PHONE_NOTIFICATION: 2,
   WAIT_FOR_PHONE_APPS: 3,
+  WAIT_FOR_PHONE_COMBINED: 4,
 };
 
 Polymer({
@@ -171,6 +172,15 @@
       computed: 'computeShouldShowDisabledDoneButton_(setupState_)',
       reflectToAttribute: true,
     },
+
+    /**
+     * Whether the combined setup for Notifications and Camera Roll is supported
+     * on the connected phone.
+     */
+    combinedSetupSupported: {
+      type: Boolean,
+      value: false,
+    },
   },
 
   /** @private {?MultiDeviceBrowserProxy} */
@@ -189,6 +199,9 @@
     this.addWebUIListener(
         'settings.onAppsAccessSetupStatusChanged',
         this.onAppsSetupStateChanged_.bind(this));
+    this.addWebUIListener(
+        'settings.onCombinedAccessSetupStatusChanged',
+        this.onCombinedSetupStateChanged_.bind(this));
     this.$.dialog.showModal();
   },
 
@@ -233,6 +246,36 @@
   },
 
   /**
+   * @param {!PermissionsSetupStatus} setupState
+   * @private
+   */
+  onCombinedSetupStateChanged_(setupState) {
+    if (this.flowState_ !== SetupFlowStatus.WAIT_FOR_PHONE_COMBINED) {
+      return;
+    }
+
+    this.setupState_ = setupState;
+    if (this.setupState_ !== PermissionsSetupStatus.COMPLETED_SUCCESSFULLY) {
+      return;
+    }
+
+    if (this.showCameraRoll) {
+      this.browserProxy_.setFeatureEnabledState(
+          MultiDeviceFeature.PHONE_HUB_CAMERA_ROLL, true);
+    }
+    if (this.showNotifications) {
+      this.browserProxy_.setFeatureEnabledState(
+          MultiDeviceFeature.PHONE_HUB_NOTIFICATIONS, true);
+    }
+
+    if (this.showAppStreaming) {
+      this.browserProxy_.attemptAppsSetup();
+      this.flowState_ = SetupFlowStatus.WAIT_FOR_PHONE_APPS;
+      this.setupState_ = PermissionsSetupStatus.CONNECTION_REQUESTED;
+    }
+  },
+
+  /**
    * @return {boolean}
    * @private
    */
@@ -298,7 +341,13 @@
         break;
     }
 
-    if (this.showNotifications) {
+    if ((this.showCameraRoll || this.showNotifications) &&
+        this.combinedSetupSupported) {
+      this.browserProxy_.attemptCombinedFeatureSetup(
+          this.showCameraRoll, this.showNotifications);
+      this.flowState_ = SetupFlowStatus.WAIT_FOR_PHONE_COMBINED;
+      this.setupState_ = PermissionsSetupStatus.CONNECTION_REQUESTED;
+    } else if (this.showNotifications && !this.combinedSetupSupported) {
       this.browserProxy_.attemptNotificationSetup();
       this.flowState_ = SetupFlowStatus.WAIT_FOR_PHONE_NOTIFICATION;
       this.setupState_ = PermissionsSetupStatus.CONNECTION_REQUESTED;
@@ -306,11 +355,6 @@
       this.browserProxy_.attemptAppsSetup();
       this.flowState_ = SetupFlowStatus.WAIT_FOR_PHONE_APPS;
       this.setupState_ = PermissionsSetupStatus.CONNECTION_REQUESTED;
-    } else if (this.showCameraRoll) {
-      // Camera Roll setup is not implemented yet
-      // Dialog fails if Camera Roll is only feature needing setup
-      this.flowState_ = SetupFlowStatus.WAIT_FOR_PHONE_NOTIFICATION;
-      this.setupState_ = PermissionsSetupStatus.TIMED_OUT_CONNECTING;
     }
   },
 
@@ -320,6 +364,8 @@
       this.browserProxy_.cancelNotificationSetup();
     } else if (this.flowState_ === SetupFlowStatus.WAIT_FOR_PHONE_APPS) {
       this.browserProxy_.cancelAppsSetup();
+    } else if (this.flowState_ === SetupFlowStatus.WAIT_FOR_PHONE_COMBINED) {
+      this.browserProxy_.cancelCombinedFeatureSetup();
     }
     this.$.dialog.close();
   },
diff --git a/chrome/browser/resources/signin/enterprise_profile_welcome/enterprise_profile_welcome_app.html b/chrome/browser/resources/signin/enterprise_profile_welcome/enterprise_profile_welcome_app.html
index 2ab226d..415076b 100644
--- a/chrome/browser/resources/signin/enterprise_profile_welcome/enterprise_profile_welcome_app.html
+++ b/chrome/browser/resources/signin/enterprise_profile_welcome/enterprise_profile_welcome_app.html
@@ -34,6 +34,7 @@
   }
 
   .action-container {
+    align-items: flex-end;
     bottom: 0;
     box-sizing: border-box;
     position: absolute;
@@ -159,6 +160,11 @@
     font-weight: normal;
   }
 
+  #buttonsContainer cr-checkbox {
+    font-size: 12px;
+    padding-inline-end: 8px;
+  }
+
   @media (prefers-color-scheme: dark) {
     .work-badge {
       border-color: var(--md-background-color);
@@ -211,10 +217,16 @@
   </template>
 </div>
 <div id="buttonsContainer" class="action-container">
+  <template is="dom-if" if="[[showLinkDataCheckbox_]]">
+    <cr-checkbox id="linkData" checked="{{linkData_}}">
+      <div>$i18n{linkDataText}</div>
+    </cr-checkbox>
+  </template>
   <cr-button id="proceedButton" class="action-button" on-click="onProceed_"
       autofocus$="[[isModalDialog_]]" disabled="[[disableProceedButton_]]">
     [[proceedLabel_]]
   </cr-button>
+  </template>
   <cr-button id="cancelButton" on-click="onCancel_">
     $i18n{cancelLabel}
   </cr-button>
diff --git a/chrome/browser/resources/signin/enterprise_profile_welcome/enterprise_profile_welcome_app.ts b/chrome/browser/resources/signin/enterprise_profile_welcome/enterprise_profile_welcome_app.ts
index a6e2838..ecfd5f4e 100644
--- a/chrome/browser/resources/signin/enterprise_profile_welcome/enterprise_profile_welcome_app.ts
+++ b/chrome/browser/resources/signin/enterprise_profile_welcome/enterprise_profile_welcome_app.ts
@@ -3,6 +3,7 @@
 // found in the LICENSE file.
 
 import 'chrome://resources/cr_elements/cr_button/cr_button.m.js';
+import 'chrome://resources/cr_elements/cr_checkbox/cr_checkbox.m.js';
 import 'chrome://resources/cr_elements/shared_vars_css.m.js';
 import 'chrome://resources/polymer/v3_0/iron-icon/iron-icon.js';
 import 'chrome://resources/cr_elements/icons.m.js';
@@ -11,6 +12,7 @@
 import './signin_vars_css.js';
 
 import {CrButtonElement} from 'chrome://resources/cr_elements/cr_button/cr_button.m.js';
+import {I18nMixin} from 'chrome://resources/js/i18n_mixin.js';
 import {loadTimeData} from 'chrome://resources/js/load_time_data.m.js';
 import {WebUIListenerMixin} from 'chrome://resources/js/web_ui_listener_mixin.js';
 import {html, PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
@@ -39,7 +41,7 @@
 }
 
 const EnterpriseProfileWelcomeAppElementBase =
-    WebUIListenerMixin(PolymerElement);
+    WebUIListenerMixin(I18nMixin(PolymerElement));
 
 export class EnterpriseProfileWelcomeAppElement extends
     EnterpriseProfileWelcomeAppElementBase {
@@ -76,12 +78,27 @@
         }
       },
 
+      showLinkDataCheckbox_: {
+        type: String,
+        reflectToAttribute: true,
+        value() {
+          return loadTimeData.getBoolean('showLinkDataCheckbox');
+        }
+      },
+
       /** The label for the button to proceed with the flow */
       proceedLabel_: String,
 
       disableProceedButton_: {
         type: Boolean,
         value: false,
+      },
+
+      linkData_: {
+        type: Boolean,
+        reflectToAttribute: true,
+        value: false,
+        observer: 'linkDataChanged_'
       }
     };
   }
@@ -93,6 +110,8 @@
   private isModalDialog_: boolean;
   private proceedLabel_: string;
   private disableProceedButton_: boolean;
+  private linkData_: boolean;
+  private defaultProceedLabel_: string;
   private enterpriseProfileWelcomeBrowserProxy_:
       EnterpriseProfileWelcomeBrowserProxy =
           EnterpriseProfileWelcomeBrowserProxyImpl.getInstance();
@@ -107,10 +126,15 @@
         info => this.setProfileInfo_(info));
   }
 
+  private linkDataChanged_(linkData: boolean) {
+    this.proceedLabel_ = linkData ? this.i18n('proceedAlternateLabel') :
+                                    this.defaultProceedLabel_;
+  }
+
   /** Called when the proceed button is clicked. */
   private onProceed_() {
     this.disableProceedButton_ = true;
-    this.enterpriseProfileWelcomeBrowserProxy_.proceed();
+    this.enterpriseProfileWelcomeBrowserProxy_.proceed(this.linkData_);
   }
 
   /** Called when the cancel button is clicked. */
@@ -124,7 +148,8 @@
     this.showEnterpriseBadge_ = info.showEnterpriseBadge;
     this.enterpriseTitle_ = info.enterpriseTitle;
     this.enterpriseInfo_ = info.enterpriseInfo;
-    this.proceedLabel_ = info.proceedLabel;
+    this.defaultProceedLabel_ = info.proceedLabel;
+    this.proceedLabel_ = this.defaultProceedLabel_;
   }
 }
 
diff --git a/chrome/browser/resources/signin/enterprise_profile_welcome/enterprise_profile_welcome_browser_proxy.ts b/chrome/browser/resources/signin/enterprise_profile_welcome/enterprise_profile_welcome_browser_proxy.ts
index da31d576..3003f38 100644
--- a/chrome/browser/resources/signin/enterprise_profile_welcome/enterprise_profile_welcome_browser_proxy.ts
+++ b/chrome/browser/resources/signin/enterprise_profile_welcome/enterprise_profile_welcome_browser_proxy.ts
@@ -28,7 +28,7 @@
   /**
    * Called when the user clicks the proceed button.
    */
-  proceed(): void;
+  proceed(linkData: boolean): void;
 
   /**
    * Called when the user clicks the cancel button.
@@ -46,8 +46,8 @@
     chrome.send('initializedWithSize', [height]);
   }
 
-  proceed() {
-    chrome.send('proceed');
+  proceed(linkData: boolean) {
+    chrome.send('proceed', [linkData]);
   }
 
   cancel() {
diff --git a/chrome/browser/site_isolation/origin_agent_cluster_browsertest.cc b/chrome/browser/site_isolation/origin_agent_cluster_browsertest.cc
index 94b8bdd..c43a0e2 100644
--- a/chrome/browser/site_isolation/origin_agent_cluster_browsertest.cc
+++ b/chrome/browser/site_isolation/origin_agent_cluster_browsertest.cc
@@ -13,6 +13,7 @@
 #include "chrome/test/base/ui_test_utils.h"
 #include "components/network_session_configurator/common/network_switches.h"
 #include "components/page_load_metrics/browser/page_load_metrics_test_waiter.h"
+#include "components/variations/active_field_trials.h"
 #include "content/public/browser/web_contents.h"
 #include "content/public/common/content_features.h"
 #include "content/public/common/content_switches.h"
@@ -219,6 +220,28 @@
 }
 
 IN_PROC_BROWSER_TEST_F(OriginAgentClusterBrowserTest,
+                       SyntheticTrialActivation) {
+  const std::string kSyntheticTrialName =
+      "ProcessIsolatedOriginAgentClusterActive";
+  const std::string kSyntheticTrialGroup = "Enabled";
+
+  GURL start_url(https_server()->GetURL("foo.com", "/iframe.html"));
+  GURL origin_keyed_url(
+      https_server()->GetURL("origin-keyed.foo.com", "/origin_key_me"));
+
+  content::WebContents* web_contents =
+      browser()->tab_strip_model()->GetActiveWebContents();
+
+  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), start_url));
+  // We won't have an active synthetic trial until we navigate to
+  // `origin_keyed_url`.
+  EXPECT_FALSE(variations::HasSyntheticTrial(kSyntheticTrialName));
+  EXPECT_TRUE(NavigateIframeToURL(web_contents, "test", origin_keyed_url));
+  EXPECT_TRUE(variations::IsInSyntheticTrialGroup(kSyntheticTrialName,
+                                                  kSyntheticTrialGroup));
+}
+
+IN_PROC_BROWSER_TEST_F(OriginAgentClusterBrowserTest,
                        ProcessCountMetricsSimple) {
   GURL start_url(https_server()->GetURL("foo.com", "/iframe.html"));
   GURL origin_keyed_url(
diff --git a/chrome/browser/tab/java/src/org/chromium/chrome/browser/tab/state/PersistedTabData.java b/chrome/browser/tab/java/src/org/chromium/chrome/browser/tab/state/PersistedTabData.java
index eeaaff5..c9d7cd9 100644
--- a/chrome/browser/tab/java/src/org/chromium/chrome/browser/tab/state/PersistedTabData.java
+++ b/chrome/browser/tab/java/src/org/chromium/chrome/browser/tab/state/PersistedTabData.java
@@ -109,7 +109,7 @@
             if (persistedTabDataFromTab.needsUpdate()) {
                 tabDataCreator.onResult((tabData) -> {
                     if (tab.isDestroyed()) {
-                        PostTask.runOrPostTask(
+                        PostTask.postTask(
                                 UiThreadTaskTraits.DEFAULT, () -> { callback.onResult(null); });
                         return;
                     }
@@ -117,11 +117,11 @@
                     if (tabData != null) {
                         setUserData(tab, clazz, tabData);
                     }
-                    PostTask.runOrPostTask(
+                    PostTask.postTask(
                             UiThreadTaskTraits.DEFAULT, () -> { callback.onResult(tabData); });
                 });
             } else {
-                PostTask.runOrPostTask(UiThreadTaskTraits.DEFAULT,
+                PostTask.postTask(UiThreadTaskTraits.DEFAULT,
                         () -> { callback.onResult(persistedTabDataFromTab); });
             }
             return;
@@ -179,15 +179,14 @@
     }
 
     private static <T extends PersistedTabData> void onPersistedTabDataResult(
-            T persistedTabData, Tab tab, Class<T> clazz, String key) {
-        if (tab.isDestroyed()) {
-            persistedTabData = null;
-        }
+            T pPersistedTabData, Tab tab, Class<T> clazz, String key) {
+        final T persistedTabData = tab.isDestroyed() ? null : pPersistedTabData;
         if (persistedTabData != null) {
             setUserData(tab, clazz, persistedTabData);
         }
         for (Callback cachedCallback : sCachedCallbacks.get(key)) {
-            cachedCallback.onResult(persistedTabData);
+            PostTask.postTask(
+                    UiThreadTaskTraits.DEFAULT, () -> cachedCallback.onResult(persistedTabData));
         }
         sCachedCallbacks.remove(key);
     }
diff --git a/chrome/browser/tab/java/src/org/chromium/chrome/browser/tab/state/ShoppingPersistedTabData.java b/chrome/browser/tab/java/src/org/chromium/chrome/browser/tab/state/ShoppingPersistedTabData.java
index 2b28f0d..0a5d61d 100644
--- a/chrome/browser/tab/java/src/org/chromium/chrome/browser/tab/state/ShoppingPersistedTabData.java
+++ b/chrome/browser/tab/java/src/org/chromium/chrome/browser/tab/state/ShoppingPersistedTabData.java
@@ -451,8 +451,7 @@
             @DelayedInitMethod
             int delayedInitMethod = getDelayedInitMethod();
             if (delayedInitMethod == DelayedInitMethod.EMPTY_RESPONSES_UNTIL_INIT) {
-                PostTask.runOrPostTask(
-                        UiThreadTaskTraits.DEFAULT, () -> { callback.onResult(null); });
+                PostTask.postTask(UiThreadTaskTraits.DEFAULT, () -> { callback.onResult(null); });
             } else if (delayedInitMethod == DelayedInitMethod.DELAY_RESPONSES_UNTIL_INIT) {
                 sShoppingDataRequests.add(new ShoppingDataRequest(tab, callback));
             } else {
@@ -466,7 +465,7 @@
         // Shopping related data is not available for incognito or Custom Tabs. For example,
         // for incognito Tabs it is not possible to call a backend service with the user's URL.
         if (tab.isIncognito() || tab.isCustomTab()) {
-            PostTask.runOrPostTask(UiThreadTaskTraits.DEFAULT, () -> { callback.onResult(null); });
+            PostTask.postTask(UiThreadTaskTraits.DEFAULT, () -> { callback.onResult(null); });
             return;
         }
         PersistedTabData.from(tab,
@@ -477,7 +476,7 @@
                                 ShoppingPersistedTabData.from(tab);
                         PostTask.postTask(TaskTraits.USER_BLOCKING_MAY_BLOCK, () -> {
                             shoppingPersistedTabData.deserializeAndLog(data);
-                            PostTask.runOrPostTask(UiThreadTaskTraits.DEFAULT,
+                            PostTask.postTask(UiThreadTaskTraits.DEFAULT,
                                     () -> { factoryCallback.onResult(shoppingPersistedTabData); });
                         });
                     });
@@ -487,8 +486,7 @@
                     if (tab.isDestroyed()
                             || getTimeSinceTabLastOpenedMs(tab)
                                     > TimeUnit.SECONDS.toMillis(getStaleTabThresholdSeconds())) {
-                        PostTask.runOrPostTask(
-                                UiThreadTaskTraits.DEFAULT, () -> supplierCallback.onResult(null));
+                        supplierCallback.onResult(null);
                         return;
                     }
                     PriceDataSnapshot previous = PersistedTabData.from(tab, USER_DATA_KEY) == null
@@ -496,10 +494,7 @@
                             : new PriceDataSnapshot(PersistedTabData.from(tab, USER_DATA_KEY));
                     ShoppingPersistedTabData.isShoppingPage(tab.getUrl(), (isShoppingPage) -> {
                         if (!isShoppingPage) {
-                            PostTask.runOrPostTask(UiThreadTaskTraits.DEFAULT,
-                                    ()
-                                            -> supplierCallback.onResult(
-                                                    getEmptyShoppingPersistedTabData(tab)));
+                            supplierCallback.onResult(getEmptyShoppingPersistedTabData(tab));
                             return;
                         }
 
@@ -510,9 +505,7 @@
                                             HintsProto.OptimizationType.PRICE_TRACKING,
                                             (decision, metadata) -> {
                                                 if (tab.isDestroyed()) {
-                                                    PostTask.runOrPostTask(
-                                                            UiThreadTaskTraits.DEFAULT,
-                                                            () -> supplierCallback.onResult(null));
+                                                    supplierCallback.onResult(null);
                                                     return;
                                                 }
                                                 if (decision != OptimizationGuideDecision.TRUE) {
@@ -520,9 +513,7 @@
                                                             getEmptyShoppingPersistedTabData(tab);
                                                     res.logPriceDropMetrics(
                                                             METRICS_IDENTIFIER_PREFIX);
-                                                    PostTask.runOrPostTask(
-                                                            UiThreadTaskTraits.DEFAULT,
-                                                            () -> supplierCallback.onResult(res));
+                                                    supplierCallback.onResult(res);
                                                     return;
                                                 }
                                                 try {
@@ -535,9 +526,7 @@
                                                             tab, priceTrackingDataProto, previous);
                                                     sptd.logPriceDropMetrics(
                                                             METRICS_IDENTIFIER_PREFIX);
-                                                    PostTask.runOrPostTask(
-                                                            UiThreadTaskTraits.DEFAULT,
-                                                            () -> supplierCallback.onResult(sptd));
+                                                    supplierCallback.onResult(sptd);
                                                 } catch (InvalidProtocolBufferException e) {
                                                     Log.i(TAG,
                                                             String.format(Locale.US,
@@ -547,18 +536,13 @@
                                                                             + "DataProto. "
                                                                             + "Details %s.",
                                                                     e));
-                                                    PostTask.runOrPostTask(
-                                                            UiThreadTaskTraits.DEFAULT,
-                                                            () -> supplierCallback.onResult(null));
+                                                    supplierCallback.onResult(null);
                                                 }
                                             });
                         } else {
                             sPageAnnotationsServiceFactory.getForLastUsedProfile().getAnnotations(
                                     tab.getUrl(), (result) -> {
-                                        PostTask.runOrPostTask(UiThreadTaskTraits.DEFAULT,
-                                                ()
-                                                        -> supplierCallback.onResult(
-                                                                build(tab, result, previous)));
+                                        supplierCallback.onResult(build(tab, result, previous));
                                     });
                         }
                     });
@@ -1136,7 +1120,7 @@
             // If Tab was destroyed we should just return null and not try and
             // create and associate {@link ShoppingPersistedTabData} with a
             // destroyed {@link Tab}.
-            PostTask.runOrPostTask(UiThreadTaskTraits.DEFAULT,
+            PostTask.postTask(UiThreadTaskTraits.DEFAULT,
                     () -> { shoppingDataRequest.callback.onResult(null); });
             processNextItemOnQueue();
             return;
diff --git a/chrome/browser/tab_contents/navigation_metrics_recorder.cc b/chrome/browser/tab_contents/navigation_metrics_recorder.cc
index 31c83ad..e86429e 100644
--- a/chrome/browser/tab_contents/navigation_metrics_recorder.cc
+++ b/chrome/browser/tab_contents/navigation_metrics_recorder.cc
@@ -76,14 +76,19 @@
   // process and register a synthetic field trial if so.  Note that this needs
   // to go before the IsInPrimaryMainFrame() check, as we want to register
   // navigations to isolated sites from both main frames and subframes.
+  auto* site_instance =
+      navigation_handle->GetRenderFrameHost()->GetSiteInstance();
   if (is_synthetic_isolation_trial_enabled_ &&
-      navigation_handle->GetRenderFrameHost()
-          ->GetSiteInstance()
-          ->RequiresDedicatedProcess()) {
+      site_instance->RequiresDedicatedProcess()) {
     ChromeMetricsServiceAccessor::RegisterSyntheticFieldTrial(
         "SiteIsolationActive", "Enabled");
   }
 
+  if (site_instance->RequiresOriginKeyedProcess()) {
+    ChromeMetricsServiceAccessor::RegisterSyntheticFieldTrial(
+        "ProcessIsolatedOriginAgentClusterActive", "Enabled");
+  }
+
   // Also register a synthetic field trial when we encounter a navigation to an
   // OOPIF.
   if (is_synthetic_isolation_trial_enabled_ &&
diff --git a/chrome/browser/ui/app_list/app_list_syncable_service.cc b/chrome/browser/ui/app_list/app_list_syncable_service.cc
index be76c38..5aeea46 100644
--- a/chrome/browser/ui/app_list/app_list_syncable_service.cc
+++ b/chrome/browser/ui/app_list/app_list_syncable_service.cc
@@ -180,32 +180,32 @@
     dict_item = pref_update->SetKey(sync_item->item_id,
                                     base::Value(base::Value::Type::DICTIONARY));
   }
-
-  dict_item->SetKey(kNameKey, base::Value(sync_item->item_name));
-  dict_item->SetKey(kParentIdKey, base::Value(sync_item->parent_id));
-  dict_item->SetKey(kPositionKey,
-                    base::Value(sync_item->item_ordinal.IsValid()
-                                    ? sync_item->item_ordinal.ToInternalValue()
-                                    : std::string()));
-  dict_item->SetKey(
+  base::Value::Dict& dict_item_dict = dict_item->GetDict();
+  dict_item_dict.Set(kNameKey, base::Value(sync_item->item_name));
+  dict_item_dict.Set(kParentIdKey, base::Value(sync_item->parent_id));
+  dict_item_dict.Set(kPositionKey,
+                     base::Value(sync_item->item_ordinal.IsValid()
+                                     ? sync_item->item_ordinal.ToInternalValue()
+                                     : std::string()));
+  dict_item_dict.Set(
       kPinPositionKey,
       base::Value(sync_item->item_pin_ordinal.IsValid()
                       ? sync_item->item_pin_ordinal.ToInternalValue()
                       : std::string()));
-  dict_item->SetKey(kTypeKey,
-                    base::Value(static_cast<int>(sync_item->item_type)));
+  dict_item_dict.Set(kTypeKey,
+                     base::Value(static_cast<int>(sync_item->item_type)));
 
   if (ash::features::IsLauncherItemColorSyncEnabled()) {
     // Handle the item color.
     if (sync_item->item_color.IsValid()) {
-      dict_item->SetKey(kBackgroundColorKey,
-                        base::Value(sync_pb::AppListSpecifics::ColorGroup_Name(
-                            sync_item->item_color.background_color())));
-      dict_item->SetKey(kHueKey, base::Value(sync_item->item_color.hue()));
-    } else if (dict_item->FindKey(kBackgroundColorKey)) {
-      dict_item->RemoveKey(kBackgroundColorKey);
-      DCHECK(dict_item->FindKey(kHueKey));
-      dict_item->RemoveKey(kHueKey);
+      dict_item_dict.Set(kBackgroundColorKey,
+                         base::Value(sync_pb::AppListSpecifics::ColorGroup_Name(
+                             sync_item->item_color.background_color())));
+      dict_item_dict.Set(kHueKey, base::Value(sync_item->item_color.hue()));
+    } else if (dict_item_dict.Find(kBackgroundColorKey)) {
+      dict_item_dict.Remove(kBackgroundColorKey);
+      DCHECK(dict_item_dict.Find(kHueKey));
+      dict_item_dict.Remove(kHueKey);
     }
   }
 }
@@ -447,8 +447,8 @@
       LOG(ERROR) << "Dictionary not found for " << item.first + ".";
       continue;
     }
-
-    absl::optional<int> type = item.second.FindIntKey(kTypeKey);
+    const base::Value::Dict& item_dict = item.second.GetDict();
+    absl::optional<int> type = item_dict.FindInt(kTypeKey);
     if (!type) {
       LOG(ERROR) << "Item type is not set in local storage for " << item.second
                  << ".";
@@ -459,17 +459,15 @@
         item.first,
         static_cast<sync_pb::AppListSpecifics::AppListItemType>(*type));
 
-    const std::string* maybe_item_name = item.second.FindStringKey(kNameKey);
+    const std::string* maybe_item_name = item_dict.FindString(kNameKey);
     if (maybe_item_name)
       sync_item->item_name = *maybe_item_name;
-    const std::string* maybe_parent_id =
-        item.second.FindStringKey(kParentIdKey);
+    const std::string* maybe_parent_id = item_dict.FindString(kParentIdKey);
     if (maybe_parent_id)
       sync_item->parent_id = *maybe_parent_id;
 
-    const std::string* position = item.second.FindStringKey(kPositionKey);
-    const std::string* pin_position =
-        item.second.FindStringKey(kPinPositionKey);
+    const std::string* position = item_dict.FindString(kPositionKey);
+    const std::string* pin_position = item_dict.FindString(kPinPositionKey);
     if (position && !position->empty())
       sync_item->item_ordinal = syncer::StringOrdinal(*position);
     if (pin_position && !pin_position->empty())
@@ -480,7 +478,7 @@
         item.second.FindKey(kBackgroundColorKey)) {
       // Retrieve the background color.
       const std::string* background_color_internal_string =
-          item.second.FindStringKey(kBackgroundColorKey);
+          item_dict.FindString(kBackgroundColorKey);
       sync_pb::AppListSpecifics::ColorGroup background_color;
       sync_pb::AppListSpecifics::ColorGroup_Parse(
           background_color_internal_string ? *background_color_internal_string
@@ -488,9 +486,9 @@
           &background_color);
 
       // Retrieve the hue.
-      DCHECK(item.second.FindKey(kHueKey));
+      DCHECK(item_dict.Find(kHueKey));
       int hue =
-          item.second.FindIntKey(kHueKey).value_or(ash::IconColor::kHueInvalid);
+          item_dict.FindInt(kHueKey).value_or(ash::IconColor::kHueInvalid);
 
       sync_item->item_color = ash::IconColor(background_color, hue);
 
diff --git a/chrome/browser/ui/app_list/arc/arc_app_list_prefs.cc b/chrome/browser/ui/app_list/arc/arc_app_list_prefs.cc
index ae1604b..7004db9 100644
--- a/chrome/browser/ui/app_list/arc/arc_app_list_prefs.cc
+++ b/chrome/browser/ui/app_list/arc/arc_app_list_prefs.cc
@@ -115,20 +115,20 @@
     DictionaryPrefUpdate update(
         prefs_, arc::prefs::kArcSetNotificationsEnabledDeferred);
     base::Value* const dict = update.Get();
-    dict->SetKey(app_id, base::Value(enabled));
+    dict->GetDict().Set(app_id, base::Value(enabled));
   }
 
   bool Get(const std::string& app_id) {
     const base::Value* dict =
         prefs_->GetDictionary(arc::prefs::kArcSetNotificationsEnabledDeferred);
-    return dict->FindBoolKey(app_id).value_or(false);
+    return dict->GetDict().FindBool(app_id).value_or(false);
   }
 
   void Remove(const std::string& app_id) {
     DictionaryPrefUpdate update(
         prefs_, arc::prefs::kArcSetNotificationsEnabledDeferred);
     base::Value* const dict = update.Get();
-    dict->RemoveKey(app_id);
+    dict->GetDict().Remove(app_id);
   }
 
  private:
@@ -219,11 +219,11 @@
          arc::IsArcPlayStoreEnabledForProfile(profile);
 }
 
-bool GetInt64FromPref(const base::Value* dict,
+bool GetInt64FromPref(const base::Value::Dict* dict,
                       const std::string& key,
                       int64_t* value) {
-  DCHECK(dict && dict->is_dict());
-  const std::string* value_str = dict->FindStringKey(key);
+  DCHECK(dict);
+  const std::string* value_str = dict->FindString(key);
   if (!value_str) {
     VLOG(2) << "Can't find key in local pref dictionary. Invalid key: " << key
             << ".";
@@ -241,12 +241,12 @@
 
 // Converts |rect| to base::Value, e.g. { 0, 100, 200, 300 }.
 base::Value RectToValueDict(const gfx::Rect& rect) {
-  base::Value dict(base::Value::Type::DICTIONARY);
-  dict.SetIntKey("x", rect.x());
-  dict.SetIntKey("y", rect.y());
-  dict.SetIntKey("width", rect.width());
-  dict.SetIntKey("height", rect.height());
-  return dict;
+  base::Value::Dict dict;
+  dict.Set("x", rect.x());
+  dict.Set("y", rect.y());
+  dict.Set("width", rect.width());
+  dict.Set("height", rect.height());
+  return base::Value(std::move(dict));
 }
 
 // Gets gfx::Rect from base::Value, e.g. { 0, 100, 200, 300 } returns
@@ -255,10 +255,10 @@
 absl::optional<gfx::Rect> RectFromDictValue(const base::Value* rect_dict) {
   if (!rect_dict)
     return absl::nullopt;
-  auto x = rect_dict->FindIntKey("x");
-  auto y = rect_dict->FindIntKey("y");
-  auto width = rect_dict->FindIntKey("width");
-  auto height = rect_dict->FindIntKey("height");
+  auto x = rect_dict->GetDict().FindInt("x");
+  auto y = rect_dict->GetDict().FindInt("y");
+  auto width = rect_dict->GetDict().FindInt("width");
+  auto height = rect_dict->GetDict().FindInt("height");
   if (!x.has_value() || !y.has_value() || !width.has_value() ||
       !height.has_value()) {
     return absl::nullopt;
@@ -268,22 +268,26 @@
 
 base::Value WindowLayoutToDict(
     const ArcAppListPrefs::WindowLayout& window_layout) {
-  base::Value dict(base::Value::Type::DICTIONARY);
-  dict.SetIntKey(kWindowSizeType, static_cast<int32_t>(window_layout.type));
-  dict.SetBoolKey(kWindowResizability, window_layout.resizable);
+  base::Value::Dict dict;
+  dict.Set(kWindowSizeType, static_cast<int32_t>(window_layout.type));
+  dict.Set(kWindowResizability, window_layout.resizable);
   if (window_layout.bounds.has_value())
-    dict.SetKey(kWindowBounds, RectToValueDict(window_layout.bounds.value()));
-  return dict;
+    dict.Set(kWindowBounds, RectToValueDict(window_layout.bounds.value()));
+
+  return base::Value(std::move(dict));
 }
 
-ArcAppListPrefs::WindowLayout WindowLayoutFromDict(const base::Value* dict) {
+ArcAppListPrefs::WindowLayout WindowLayoutFromDict(
+    const base::Value::Dict* dict) {
   if (!dict)
     return ArcAppListPrefs::WindowLayout();
+
+  const base::Value* window_bounds = dict->Find(kWindowBounds);
   return ArcAppListPrefs::WindowLayout(
       static_cast<arc::mojom::WindowSizeType>(
-          dict->FindIntKey(kWindowSizeType).value_or(0)),
-      dict->FindBoolKey(kWindowResizability).value_or(true),
-      RectFromDictValue(dict->FindKey(kWindowBounds)));
+          dict->FindInt(kWindowSizeType).value_or(0)),
+      dict->FindBool(kWindowResizability).value_or(true),
+      RectFromDictValue(window_bounds));
 }
 
 ArcAppListPrefs::WindowLayout WindowLayoutFromApp(
@@ -405,15 +409,13 @@
 
   for (const auto it : apps->DictItems()) {
     const base::Value& value = it.second;
-    const base::Value* installed_package_name =
-        value.FindKeyOfType(kPackageName, base::Value::Type::STRING);
-    if (!installed_package_name ||
-        installed_package_name->GetString() != package_name)
+    const std::string* installed_package_name =
+        value.GetDict().FindString(kPackageName);
+    if (!installed_package_name || *installed_package_name != package_name)
       continue;
 
-    const base::Value* activity_name =
-        value.FindKeyOfType(kActivity, base::Value::Type::STRING);
-    return activity_name ? GetAppId(package_name, activity_name->GetString())
+    const std::string* activity_name = value.GetDict().FindString(kActivity);
+    return activity_name ? GetAppId(package_name, *activity_name)
                          : std::string();
   }
   return std::string();
@@ -717,11 +719,11 @@
   if (!packages)
     return nullptr;
 
-  const base::Value* package = packages->FindDictKey(package_name);
+  const base::Value::Dict* package = packages->GetDict().FindDict(package_name);
   if (!package)
     return nullptr;
 
-  if (package->FindBoolKey(kUninstalled).value_or(false))
+  if (package->FindBool(kUninstalled).value_or(false))
     return nullptr;
 
   int64_t last_backup_android_id = 0;
@@ -731,7 +733,7 @@
 
   GetInt64FromPref(package, kLastBackupAndroidId, &last_backup_android_id);
   GetInt64FromPref(package, kLastBackupTime, &last_backup_time);
-  const base::Value* permission_val = package->FindKey(kPermissionStates);
+  const base::Value* permission_val = package->Find(kPermissionStates);
   if (permission_val) {
     const base::DictionaryValue* permission_dict = nullptr;
     permission_val->GetAsDictionary(&permission_dict);
@@ -747,12 +749,12 @@
 
       const base::DictionaryValue* permission_state_dict;
       if (permission_state.GetAsDictionary(&permission_state_dict)) {
-        bool granted =
-            permission_state_dict->FindBoolKey(kPermissionStateGranted)
-                .value_or(false);
-        bool managed =
-            permission_state_dict->FindBoolKey(kPermissionStateManaged)
-                .value_or(false);
+        bool granted = permission_state_dict->GetDict()
+                           .FindBool(kPermissionStateGranted)
+                           .value_or(false);
+        bool managed = permission_state_dict->GetDict()
+                           .FindBool(kPermissionStateManaged)
+                           .value_or(false);
         arc::mojom::AppPermission permission =
             static_cast<arc::mojom::AppPermission>(permission_type);
         permissions.emplace(permission,
@@ -764,12 +766,11 @@
   }
 
   return std::make_unique<PackageInfo>(
-      package_name, package->FindIntKey(kPackageVersion).value_or(0),
+      package_name, package->FindInt(kPackageVersion).value_or(0),
       last_backup_android_id, last_backup_time,
-      package->FindBoolKey(kShouldSync).value_or(false),
-      package->FindBoolKey(kSystem).value_or(false),
-      package->FindBoolKey(kVPNProvider).value_or(false),
-      std::move(permissions));
+      package->FindBool(kShouldSync).value_or(false),
+      package->FindBool(kSystem).value_or(false),
+      package->FindBool(kVPNProvider).value_or(false), std::move(permissions));
 }
 
 bool ArcAppListPrefs::IsPackageInstalled(
@@ -830,25 +831,25 @@
   const base::Value* apps = prefs_->GetDictionary(arc::prefs::kArcApps);
   if (!apps)
     return nullptr;
-  const base::Value* app = apps->FindDictKey(app_id);
-  if (!app)
+  const base::Value::Dict* app_dict = apps->GetDict().FindDict(app_id);
+  if (!app_dict)
     return nullptr;
 
   bool notifications_enabled =
-      app->FindBoolKey(kNotificationsEnabled).value_or(true);
+      app_dict->FindBool(kNotificationsEnabled).value_or(true);
   auto resize_lock_state = static_cast<arc::mojom::ArcResizeLockState>(
-      app->FindIntKey(kResizeLockState)
+      app_dict->FindInt(kResizeLockState)
           .value_or(
               static_cast<int32_t>(arc::mojom::ArcResizeLockState::UNDEFINED)));
-  const bool shortcut = app->FindBoolKey(kShortcut).value_or(false);
-  const bool launchable = app->FindBoolKey(kLaunchable).value_or(true);
+  const bool shortcut = app_dict->FindBool(kShortcut).value_or(false);
+  const bool launchable = app_dict->FindBool(kLaunchable).value_or(true);
 
-  const std::string* maybe_name = app->FindStringKey(kName);
-  const std::string* maybe_package_name = app->FindStringKey(kPackageName);
-  const std::string* maybe_activity = app->FindStringKey(kActivity);
-  const std::string* maybe_intent_uri = app->FindStringKey(kIntentUri);
+  const std::string* maybe_name = app_dict->FindString(kName);
+  const std::string* maybe_package_name = app_dict->FindString(kPackageName);
+  const std::string* maybe_activity = app_dict->FindString(kActivity);
+  const std::string* maybe_intent_uri = app_dict->FindString(kIntentUri);
   const std::string* maybe_icon_resource_id =
-      app->FindStringKey(kIconResourceId);
+      app_dict->FindString(kIconResourceId);
 
   std::string name = maybe_name ? *maybe_name : std::string();
   std::string package_name =
@@ -864,7 +865,7 @@
 
   int64_t last_launch_time_internal = 0;
   base::Time last_launch_time;
-  if (GetInt64FromPref(app, kLastLaunchTime, &last_launch_time_internal)) {
+  if (GetInt64FromPref(app_dict, kLastLaunchTime, &last_launch_time_internal)) {
     last_launch_time = base::Time::FromInternalValue(last_launch_time_internal);
   }
 
@@ -873,16 +874,16 @@
     notifications_enabled = deferred;
 
   WindowLayout window_layout =
-      WindowLayoutFromDict(app->FindDictKey(kWindowLayout));
+      WindowLayoutFromDict(app_dict->FindDict(kWindowLayout));
 
   return std::make_unique<AppInfo>(
       name, package_name, activity, intent_uri, icon_resource_id,
       last_launch_time, GetInstallTime(app_id),
-      app->FindBoolKey(kSticky).value_or(false), notifications_enabled,
+      app_dict->FindBool(kSticky).value_or(false), notifications_enabled,
       resize_lock_state,
-      app->FindBoolKey(kResizeLockNeedsConfirmation).value_or(true),
+      app_dict->FindBool(kResizeLockNeedsConfirmation).value_or(true),
       window_layout, ready_apps_.count(app_id) > 0 /* ready */,
-      app->FindBoolKey(kSuspended).value_or(false),
+      app_dict->FindBool(kSuspended).value_or(false),
       launchable && arc::ShouldShowInLauncher(app_id), shortcut, launchable);
 }
 
@@ -947,7 +948,7 @@
   arc::ArcAppScopedPrefUpdate update(prefs_, app_id, arc::prefs::kArcApps);
   base::Value* app_dict = update.Get();
   const std::string string_value = base::NumberToString(time.ToInternalValue());
-  app_dict->SetStringKey(kLastLaunchTime, string_value);
+  app_dict->GetDict().Set(kLastLaunchTime, string_value);
 
   for (auto& observer : observer_list_)
     observer.OnAppLastLaunchTimeUpdated(app_id);
@@ -1118,7 +1119,7 @@
 
   arc::ArcAppScopedPrefUpdate update(prefs_, app_id, arc::prefs::kArcApps);
   base::Value* app_dict = update.Get();
-  app_dict->SetIntKey(kResizeLockState, static_cast<int32_t>(state));
+  app_dict->GetDict().Set(kResizeLockState, static_cast<int32_t>(state));
 
   NotifyAppStatesChanged(app_id);
 }
@@ -1144,7 +1145,7 @@
 
   arc::ArcAppScopedPrefUpdate update(prefs_, app_id, arc::prefs::kArcApps);
   base::Value* app_dict = update.Get();
-  app_dict->SetBoolKey(kResizeLockNeedsConfirmation, is_needed);
+  app_dict->GetDict().Set(kResizeLockNeedsConfirmation, is_needed);
 }
 
 int ArcAppListPrefs::GetShowSplashScreenDialogCount() const {
@@ -1198,7 +1199,7 @@
   }
   arc::ArcAppScopedPrefUpdate update(prefs_, package_name,
                                      arc::prefs::kArcPackages);
-  return update.Get()->FindKey(key);
+  return update.Get()->GetDict().Find(key);
 }
 
 void ArcAppListPrefs::SetPackagePrefs(const std::string& package_name,
@@ -1210,7 +1211,7 @@
   }
   arc::ArcAppScopedPrefUpdate update(prefs_, package_name,
                                      arc::prefs::kArcPackages);
-  update.Get()->SetKey(key, std::move(value));
+  update.Get()->GetDict().Set(key, std::move(value));
 }
 
 void ArcAppListPrefs::SetDefaultAppsReadyCallback(base::OnceClosure callback) {
@@ -1337,30 +1338,28 @@
       GetResizeLockNeedsConfirmation(app_id);
 
   arc::ArcAppScopedPrefUpdate update(prefs_, app_id, arc::prefs::kArcApps);
-  base::Value* app_dict = update.Get();
-  app_dict->SetStringKey(kName, updated_name);
-  app_dict->SetStringKey(kPackageName, package_name);
-  app_dict->SetStringKey(kActivity, activity);
-  app_dict->SetStringKey(kIntentUri, intent_uri);
-  app_dict->SetStringKey(kIconResourceId, icon_resource_id);
-  app_dict->SetBoolKey(kSuspended, suspended);
-  app_dict->SetBoolKey(kSticky, sticky);
-  app_dict->SetBoolKey(kNotificationsEnabled, notifications_enabled);
-  app_dict->SetIntKey(kResizeLockState,
-                      static_cast<int32_t>(resize_lock_state));
-  app_dict->SetBoolKey(kResizeLockNeedsConfirmation,
-                       resize_lock_needs_confirmation);
-  app_dict->SetBoolKey(kShortcut, shortcut);
-  app_dict->SetBoolKey(kLaunchable, launchable);
+  base::Value::Dict& app_dict = update.Get()->GetDict();
+  app_dict.Set(kName, updated_name);
+  app_dict.Set(kPackageName, package_name);
+  app_dict.Set(kActivity, activity);
+  app_dict.Set(kIntentUri, intent_uri);
+  app_dict.Set(kIconResourceId, icon_resource_id);
+  app_dict.Set(kSuspended, suspended);
+  app_dict.Set(kSticky, sticky);
+  app_dict.Set(kNotificationsEnabled, notifications_enabled);
+  app_dict.Set(kResizeLockState, static_cast<int32_t>(resize_lock_state));
+  app_dict.Set(kResizeLockNeedsConfirmation, resize_lock_needs_confirmation);
+  app_dict.Set(kShortcut, shortcut);
+  app_dict.Set(kLaunchable, launchable);
 
-  app_dict->SetKey(kWindowLayout, WindowLayoutToDict(initial_window_layout));
+  app_dict.Set(kWindowLayout, WindowLayoutToDict(initial_window_layout));
 
   // Note the install time is the first time the Chrome OS sees the app, not the
   // actual install time in Android side.
   if (GetInstallTime(app_id).is_null()) {
     std::string install_time_str =
         base::NumberToString(base::Time::Now().ToInternalValue());
-    app_dict->SetStringKey(kInstallTime, install_time_str);
+    app_dict.Set(kInstallTime, install_time_str);
   }
 
   const bool was_disabled = ready_apps_.count(app_id) == 0;
@@ -1406,7 +1405,7 @@
     // Invalidate app icons in case it was already registered, becomes ready and
     // icon version is updated. This allows to use previous icons until new
     // icons are been prepared.
-    const base::Value* existing_version = app_dict->FindKey(kIconVersion);
+    const base::Value* existing_version = app_dict.Find(kIconVersion);
     if (was_tracked && (!existing_version ||
                         existing_version->GetInt() != current_icons_version)) {
       VLOG(1) << "Invalidate icons for " << app_id << " from "
@@ -1415,7 +1414,7 @@
       InvalidateAppIcons(app_id);
     }
 
-    app_dict->SetKey(kIconVersion, base::Value(current_icons_version));
+    app_dict.Set(kIconVersion, base::Value(current_icons_version));
 
     if (arc::IsArcForceCacheAppIcon() && app_id != arc::kPlayStoreAppId) {
       // Request full set of app icons.
@@ -1451,7 +1450,7 @@
   // Remove from prefs.
   DictionaryPrefUpdate update(prefs_, arc::prefs::kArcApps);
   base::Value* apps = update.Get();
-  const bool removed = apps->RemoveKey(app_id);
+  const bool removed = apps->GetDict().Remove(app_id);
   DCHECK(removed);
 
   // |tracked_apps_| contains apps that are reported externally as available.
@@ -1492,38 +1491,37 @@
 
   arc::ArcAppScopedPrefUpdate update(prefs_, package_name,
                                      arc::prefs::kArcPackages);
-  base::Value* package_dict = update.Get();
+  base::Value::Dict& package_dict = update.Get()->GetDict();
   const std::string id_str =
       base::NumberToString(package.last_backup_android_id);
   const std::string time_str = base::NumberToString(package.last_backup_time);
 
-  int old_package_version =
-      package_dict->FindIntKey(kPackageVersion).value_or(-1);
-  package_dict->SetBoolKey(kShouldSync, package.sync);
-  package_dict->SetIntKey(kPackageVersion, package.package_version);
-  package_dict->SetStringKey(kLastBackupAndroidId, id_str);
-  package_dict->SetStringKey(kLastBackupTime, time_str);
-  package_dict->SetBoolKey(kSystem, package.system);
-  package_dict->SetBoolKey(kUninstalled, false);
-  package_dict->SetBoolKey(kVPNProvider, package.vpn_provider);
+  int old_package_version = package_dict.FindInt(kPackageVersion).value_or(-1);
+  package_dict.Set(kShouldSync, package.sync);
+  package_dict.Set(kPackageVersion, package.package_version);
+  package_dict.Set(kLastBackupAndroidId, id_str);
+  package_dict.Set(kLastBackupTime, time_str);
+  package_dict.Set(kSystem, package.system);
+  package_dict.Set(kUninstalled, false);
+  package_dict.Set(kVPNProvider, package.vpn_provider);
 
   base::DictionaryValue permissions_dict;
   if (package.permission_states.has_value()) {
     // Support new format
     for (const auto& permission : package.permission_states.value()) {
       base::DictionaryValue permission_state_dict;
-      permission_state_dict.SetBoolKey(kPermissionStateGranted,
-                                       permission.second->granted);
-      permission_state_dict.SetBoolKey(kPermissionStateManaged,
-                                       permission.second->managed);
-      permissions_dict.SetKey(
+      permission_state_dict.GetDict().Set(kPermissionStateGranted,
+                                          permission.second->granted);
+      permission_state_dict.GetDict().Set(kPermissionStateManaged,
+                                          permission.second->managed);
+      permissions_dict.GetDict().Set(
           base::NumberToString(static_cast<int64_t>(permission.first)),
           std::move(permission_state_dict));
     }
-    package_dict->SetKey(kPermissionStates, std::move(permissions_dict));
+    package_dict.Set(kPermissionStates, std::move(permissions_dict));
   } else {
     // Remove kPermissionStates from dict if there are no permissions.
-    package_dict->RemoveKey(kPermissionStates);
+    package_dict.Remove(kPermissionStates);
   }
 
   if (old_package_version == -1 ||
@@ -1537,7 +1535,8 @@
 void ArcAppListPrefs::RemovePackageFromPrefs(const std::string& package_name) {
   DictionaryPrefUpdate(prefs_, arc::prefs::kArcPackages)
       .Get()
-      ->RemoveKey(package_name);
+      ->GetDict()
+      .Remove(package_name);
 }
 
 void ArcAppListPrefs::OnAppListRefreshed(
@@ -1702,7 +1701,7 @@
     if (shelf_controller) {
       int pin_index =
           shelf_controller->PinnedItemIndexByAppID(*apps_to_remove.begin());
-      package_dict->SetIntKey(kPinIndex, pin_index);
+      package_dict->GetDict().Set(kPinIndex, pin_index);
     }
   }
 
@@ -1735,14 +1734,15 @@
     }
 
     const std::string* installed_package_name =
-        app.second.FindStringKey(kPackageName);
+        app.second.GetDict().FindString(kPackageName);
     const std::string* installed_intent_uri =
-        app.second.FindStringKey(kIntentUri);
+        app.second.GetDict().FindString(kIntentUri);
     if (!installed_package_name || !installed_intent_uri) {
       VLOG(2) << "Failed to extract information for " << app.first << ".";
       continue;
     }
-    const bool shortcut = app.second.FindBoolKey(kShortcut).value_or(false);
+    const bool shortcut =
+        app.second.GetDict().FindBool(kShortcut).value_or(false);
     if (!shortcut || *installed_package_name != package_name ||
         *installed_intent_uri != intent_uri) {
       continue;
@@ -1777,7 +1777,8 @@
       continue;
     }
 
-    const std::string* app_package = app.second.FindStringKey(kPackageName);
+    const std::string* app_package =
+        app.second.GetDict().FindString(kPackageName);
     if (!app_package) {
       LOG(ERROR) << "App is malformed: " << app.first;
       continue;
@@ -1787,13 +1788,13 @@
       continue;
 
     if (!include_shortcuts) {
-      if (app.second.FindBoolKey(kShortcut).value_or(false))
+      if (app.second.GetDict().FindBool(kShortcut).value_or(false))
         continue;
     }
 
     if (include_only_launchable_apps) {
       // Filter out non-lauchable apps.
-      if (!app.second.FindBoolKey(kLaunchable).value_or(false))
+      if (!app.second.GetDict().FindBool(kLaunchable).value_or(false))
         continue;
     }
 
@@ -1918,7 +1919,7 @@
       continue;
     }
     const std::string* app_package_name =
-        app.second.FindStringKey(kPackageName);
+        app.second.GetDict().FindString(kPackageName);
     if (!app_package_name) {
       NOTREACHED();
       continue;
@@ -1928,7 +1929,7 @@
     }
     arc::ArcAppScopedPrefUpdate update(prefs_, app.first, arc::prefs::kArcApps);
     base::Value* updating_app_dict = update.Get();
-    updating_app_dict->SetBoolKey(kNotificationsEnabled, enabled);
+    updating_app_dict->GetDict().Set(kNotificationsEnabled, enabled);
   }
   for (auto& observer : observer_list_)
     observer.OnNotificationsEnabledChanged(package_name, enabled);
@@ -2020,7 +2021,7 @@
     }
 
     const bool uninstalled =
-        package.second.FindBoolKey(kUninstalled).value_or(false);
+        package.second.GetDict().FindBool(kUninstalled).value_or(false);
     if (installed != !uninstalled)
       continue;
 
@@ -2039,7 +2040,7 @@
   if (!app)
     return base::Time();
 
-  const std::string* install_time_str = app->FindStringKey(kInstallTime);
+  const std::string* install_time_str = app->GetDict().FindString(kInstallTime);
   if (!install_time_str)
     return base::Time();
 
diff --git a/chrome/browser/ui/app_list/arc/arc_default_app_list.cc b/chrome/browser/ui/app_list/arc/arc_default_app_list.cc
index 73ac703..e8e7446 100644
--- a/chrome/browser/ui/app_list/arc/arc_default_app_list.cc
+++ b/chrome/browser/ui/app_list/arc/arc_default_app_list.cc
@@ -6,6 +6,9 @@
 
 #include <string.h>
 
+#include <utility>
+#include <vector>
+
 #include "ash/components/arc/arc_util.h"
 #include "base/barrier_closure.h"
 #include "base/bind.h"
@@ -87,12 +90,10 @@
     base::Value app_info =
         base::Value::FromUniquePtrValue(std::move(app_info_ptr));
 
-    CHECK(app_info.is_dict());
-
-    auto* name = app_info.FindStringKey(kName);
-    auto* package_name = app_info.FindStringKey(kPackageName);
-    auto* activity = app_info.FindStringKey(kActivity);
-    auto* app_path = app_info.FindStringKey(kAppPath);
+    auto* name = app_info.GetDict().FindString(kName);
+    auto* package_name = app_info.GetDict().FindString(kPackageName);
+    auto* activity = app_info.GetDict().FindString(kActivity);
+    auto* app_path = app_info.GetDict().FindString(kAppPath);
     bool oem = app_info.FindBoolPath(kOem).value_or(false);
 
     if (!name || !package_name || !activity || !app_path || name->empty() ||
@@ -328,7 +329,8 @@
   // Store hidden flag.
   arc::ArcAppScopedPrefUpdate(profile_->GetPrefs(), app_id, kDefaultApps)
       .Get()
-      ->SetBoolKey(kHidden, hidden);
+      ->GetDict()
+      .Set(kHidden, hidden);
 }
 
 void ArcDefaultAppList::SetAppsHiddenForPackage(
diff --git a/chrome/browser/ui/app_list/search/files/item_suggest_cache.cc b/chrome/browser/ui/app_list/search/files/item_suggest_cache.cc
index fd74950..9b59e0f 100644
--- a/chrome/browser/ui/app_list/search/files/item_suggest_cache.cc
+++ b/chrome/browser/ui/app_list/search/files/item_suggest_cache.cc
@@ -108,7 +108,7 @@
                                       const std::string& key) {
   if (!value->is_dict())
     return absl::nullopt;
-  const std::string* field = value->FindStringKey(key);
+  const std::string* field = value->GetDict().FindString(key);
   if (!field)
     return absl::nullopt;
   return *field;
diff --git a/chrome/browser/ui/ash/chrome_shell_delegate.cc b/chrome/browser/ui/ash/chrome_shell_delegate.cc
index 6900baa..fd19a4b6 100644
--- a/chrome/browser/ui/ash/chrome_shell_delegate.cc
+++ b/chrome/browser/ui/ash/chrome_shell_delegate.cc
@@ -132,7 +132,7 @@
 }
 
 scoped_refptr<network::SharedURLLoaderFactory>
-ChromeShellDelegate::GetGeolocationSharedURLLoaderFactory() const {
+ChromeShellDelegate::GetGeolocationUrlLoaderFactory() const {
   return g_browser_process->shared_url_loader_factory();
 }
 
diff --git a/chrome/browser/ui/ash/chrome_shell_delegate.h b/chrome/browser/ui/ash/chrome_shell_delegate.h
index 57cb4704..efd71ee 100644
--- a/chrome/browser/ui/ash/chrome_shell_delegate.h
+++ b/chrome/browser/ui/ash/chrome_shell_delegate.h
@@ -33,7 +33,7 @@
   std::unique_ptr<ash::DesksTemplatesDelegate> CreateDesksTemplatesDelegate()
       const override;
   scoped_refptr<network::SharedURLLoaderFactory>
-  GetGeolocationSharedURLLoaderFactory() const override;
+  GetGeolocationUrlLoaderFactory() const override;
   void OpenKeyboardShortcutHelpPage() const override;
   bool CanGoBack(gfx::NativeWindow window) const override;
   void SetTabScrubberChromeOSEnabled(bool enabled) override;
diff --git a/chrome/browser/ui/ash/shelf/chrome_shelf_controller_util.cc b/chrome/browser/ui/ash/shelf/chrome_shelf_controller_util.cc
index 85c4771..55c0fd1 100644
--- a/chrome/browser/ui/ash/shelf/chrome_shelf_controller_util.cc
+++ b/chrome/browser/ui/ash/shelf/chrome_shelf_controller_util.cc
@@ -133,8 +133,8 @@
     if (!policy_dict_entry.is_dict())
       return AppListControllerDelegate::PIN_EDITABLE;
 
-    const std::string* policy_entry =
-        policy_dict_entry.FindStringKey(ChromeShelfPrefs::kPinnedAppsPrefAppIDKey);
+    const std::string* policy_entry = policy_dict_entry.GetDict().FindString(
+        ChromeShelfPrefs::kPinnedAppsPrefAppIDKey);
     if (!policy_entry)
       return AppListControllerDelegate::PIN_EDITABLE;
 
diff --git a/chrome/browser/ui/ash/shelf/chrome_shelf_prefs.cc b/chrome/browser/ui/ash/shelf/chrome_shelf_prefs.cc
index 30c82f4c..b1516ede 100644
--- a/chrome/browser/ui/ash/shelf/chrome_shelf_prefs.cc
+++ b/chrome/browser/ui/ash/shelf/chrome_shelf_prefs.cc
@@ -235,7 +235,7 @@
   for (const auto& policy_dict_entry : policy_apps->GetListDeprecated()) {
     const std::string* policy_entry =
         policy_dict_entry.is_dict()
-            ? policy_dict_entry.FindStringKey(
+            ? policy_dict_entry.GetDict().FindString(
                   ChromeShelfPrefs::kPinnedAppsPrefAppIDKey)
             : nullptr;
 
diff --git a/chrome/browser/ui/ash/thumbnail_loader.cc b/chrome/browser/ui/ash/thumbnail_loader.cc
index f091835..732641f0 100644
--- a/chrome/browser/ui/ash/thumbnail_loader.cc
+++ b/chrome/browser/ui/ash/thumbnail_loader.cc
@@ -4,6 +4,9 @@
 
 #include "chrome/browser/ui/ash/thumbnail_loader.h"
 
+#include <algorithm>
+#include <utility>
+
 #include "ash/public/cpp/image_downloader.h"
 #include "base/bind.h"
 #include "base/callback_helpers.h"
@@ -195,8 +198,8 @@
   }
 
   const std::string* received_request_id =
-      result.value->FindStringKey("taskId");
-  const std::string* data = result.value->FindStringKey("data");
+      result.value->GetDict().FindString("taskId");
+  const std::string* data = result.value->GetDict().FindString("data");
 
   if (!data || !received_request_id || *received_request_id != request_id) {
     std::move(callback).Run("");
@@ -401,15 +404,16 @@
   // Generate an image loader request. The request type is defined in
   // ui/file_manager/image_loader/load_image_request.js.
   base::Value request_value(base::Value::Type::DICTIONARY);
-  request_value.SetKey("taskId", base::Value(request_id.ToString()));
-  request_value.SetKey("url", base::Value(thumbnail_url.spec()));
-  request_value.SetKey("timestamp", base::TimeToValue(file_info.last_modified));
+  base::Value::Dict& request_dict = request_value.GetDict();
+  request_dict.Set("taskId", base::Value(request_id.ToString()));
+  request_dict.Set("url", base::Value(thumbnail_url.spec()));
+  request_dict.Set("timestamp", base::TimeToValue(file_info.last_modified));
   // TODO(crbug.com/2650014) : Add an arg to set this to false for sharesheet.
-  request_value.SetBoolKey("cache", true);
-  request_value.SetBoolKey("crop", true);
-  request_value.SetKey("priority", base::Value(1));
-  request_value.SetKey("width", base::Value(size));
-  request_value.SetKey("height", base::Value(size));
+  request_dict.Set("cache", true);
+  request_dict.Set("crop", true);
+  request_dict.Set("priority", base::Value(1));
+  request_dict.Set("width", base::Value(size));
+  request_dict.Set("height", base::Value(size));
 
   std::string request_message;
   base::JSONWriter::Write(request_value, &request_message);
diff --git a/chrome/browser/ui/ash/wallpaper_controller_client_impl.cc b/chrome/browser/ui/ash/wallpaper_controller_client_impl.cc
index 880d24f0..53fc6aa 100644
--- a/chrome/browser/ui/ash/wallpaper_controller_client_impl.cc
+++ b/chrome/browser/ui/ash/wallpaper_controller_client_impl.cc
@@ -4,7 +4,10 @@
 
 #include "chrome/browser/ui/ash/wallpaper_controller_client_impl.h"
 
+#include <algorithm>
+#include <string>
 #include <utility>
+#include <vector>
 
 #include "ash/components/cryptohome/system_salt_getter.h"
 #include "ash/components/settings/cros_settings_names.h"
@@ -183,7 +186,8 @@
     return std::string();
 
   const auto* daily_refresh_info_string =
-      read_result.settings().FindStringKey(kChromeAppDailyRefreshInfoPref);
+      read_result.settings().GetDict().FindString(
+          kChromeAppDailyRefreshInfoPref);
 
   if (!daily_refresh_info_string)
     return std::string();
@@ -195,7 +199,7 @@
     return std::string();
 
   const auto* collection_id =
-      daily_refresh_info->FindStringKey(kChromeAppCollectionId);
+      daily_refresh_info->GetDict().FindString(kChromeAppCollectionId);
 
   if (!collection_id)
     return std::string();
diff --git a/chrome/browser/ui/autofill/autofill_popup_controller_utils.cc b/chrome/browser/ui/autofill/autofill_popup_controller_utils.cc
index e13e0b2..9aebffb 100644
--- a/chrome/browser/ui/autofill/autofill_popup_controller_utils.cc
+++ b/chrome/browser/ui/autofill/autofill_popup_controller_utils.cc
@@ -33,7 +33,6 @@
     {kTroyCard, IDR_AUTOFILL_CC_TROY},
     {kUnionPay, IDR_AUTOFILL_CC_UNIONPAY},
     {kVisaCard, IDR_AUTOFILL_CC_VISA},
-    {kGoogleIssuedCard, IDR_AUTOFILL_GOOGLE_ISSUED_CARD},
 #if BUILDFLAG(IS_ANDROID)
     {"httpWarning", IDR_ANDROID_AUTOFILL_HTTP_WARNING},
     {"httpsInvalid", IDR_ANDROID_AUTOFILL_HTTPS_INVALID_WARNING},
diff --git a/chrome/browser/ui/color/chrome_color_id.h b/chrome/browser/ui/color/chrome_color_id.h
index 2bca4c2..7b4a48f 100644
--- a/chrome/browser/ui/color/chrome_color_id.h
+++ b/chrome/browser/ui/color/chrome_color_id.h
@@ -52,6 +52,8 @@
   E_CPONLY(kColorDesktopMediaTabListBorder) \
   E_CPONLY(kColorDesktopMediaTabListPreviewBackground) \
   /* Download shelf colors. */ \
+  E_CPONLY(kColorDownloadItemProgressRingBackground) \
+  E_CPONLY(kColorDownloadItemProgressRingForeground) \
   E(kColorDownloadShelfBackground, ThemeProperties::COLOR_DOWNLOAD_SHELF) \
   E(kColorDownloadShelfButtonBackground, \
     ThemeProperties::COLOR_DOWNLOAD_SHELF_BUTTON_BACKGROUND) \
diff --git a/chrome/browser/ui/color/chrome_color_mixer.cc b/chrome/browser/ui/color/chrome_color_mixer.cc
index ef17095..e2270ebb 100644
--- a/chrome/browser/ui/color/chrome_color_mixer.cc
+++ b/chrome/browser/ui/color/chrome_color_mixer.cc
@@ -226,6 +226,9 @@
   mixer[kColorCapturedTabContentsBorder] = {ui::kColorAccent};
   mixer[kColorDesktopMediaTabListBorder] = {ui::kColorMidground};
   mixer[kColorDesktopMediaTabListPreviewBackground] = {ui::kColorMidground};
+  mixer[kColorDownloadItemProgressRingBackground] = {
+      ui::SetAlpha(kColorDownloadItemProgressRingForeground, 0x33)};
+  mixer[kColorDownloadItemProgressRingForeground] = {ui::kColorThrobber};
   mixer[kColorDownloadShelfBackground] = {kColorToolbar};
   mixer[kColorDownloadShelfButtonBackground] = {kColorDownloadShelfBackground};
   mixer[kColorDownloadShelfButtonText] =
diff --git a/chrome/browser/ui/global_media_controls/cast_media_notification_item.cc b/chrome/browser/ui/global_media_controls/cast_media_notification_item.cc
index 97c45118..a8d4d499 100644
--- a/chrome/browser/ui/global_media_controls/cast_media_notification_item.cc
+++ b/chrome/browser/ui/global_media_controls/cast_media_notification_item.cc
@@ -155,7 +155,7 @@
       profile_(profile),
       session_controller_(std::move(session_controller)),
       media_route_id_(route.media_route_id()),
-      is_local_presentation_(route.is_local_presentation()),
+      route_is_local_(route.is_local()),
       image_downloader_(
           profile,
           base::BindRepeating(&CastMediaNotificationItem::ImageChanged,
diff --git a/chrome/browser/ui/global_media_controls/cast_media_notification_item.h b/chrome/browser/ui/global_media_controls/cast_media_notification_item.h
index c2290cfe..6734ada 100644
--- a/chrome/browser/ui/global_media_controls/cast_media_notification_item.h
+++ b/chrome/browser/ui/global_media_controls/cast_media_notification_item.h
@@ -78,7 +78,7 @@
   }
   Profile* profile() { return profile_; }
   bool is_active() const { return is_active_; }
-  bool is_local_presentation() const { return is_local_presentation_; }
+  bool route_is_local() const { return route_is_local_; }
 
   base::WeakPtr<CastMediaNotificationItem> GetWeakPtr() {
     return weak_ptr_factory_.GetWeakPtr();
@@ -139,7 +139,8 @@
 
   std::unique_ptr<CastMediaSessionController> session_controller_;
   const media_router::MediaRoute::Id media_route_id_;
-  const bool is_local_presentation_;
+  // True if the route is started from the |profile_| on the current device.
+  const bool route_is_local_;
   ImageDownloader image_downloader_;
   media_session::MediaMetadata metadata_;
   std::vector<media_session::mojom::MediaSessionAction> actions_;
diff --git a/chrome/browser/ui/global_media_controls/cast_media_notification_producer.cc b/chrome/browser/ui/global_media_controls/cast_media_notification_producer.cc
index 9432c69..d90b2f3f 100644
--- a/chrome/browser/ui/global_media_controls/cast_media_notification_producer.cc
+++ b/chrome/browser/ui/global_media_controls/cast_media_notification_producer.cc
@@ -14,10 +14,15 @@
 #include "components/media_message_center/media_notification_util.h"
 #include "components/media_router/browser/media_router.h"
 #include "components/media_router/browser/media_router_factory.h"
+#include "components/media_router/common/pref_names.h"
 #include "components/media_router/common/providers/cast/cast_media_source.h"
+#include "components/prefs/pref_service.h"
 
 namespace {
 
+// Returns false if a notification item shouldn't be created for |route|.
+// If a route should be hidden, it's not possible to create an item
+// for this route until the next time |OnModuleUpdated()| is called.
 bool ShouldHideNotification(const raw_ptr<Profile> profile,
                             const media_router::MediaRoute& route) {
   // TODO(crbug.com/1195382): Display multizone group route.
@@ -26,16 +31,18 @@
   }
 
   if (media_router::GlobalMediaControlsCastStartStopEnabled(profile)) {
-    // Hide a route if it's not for display or it's a mirroring route.
+    // Hide a route if it's a mirroring route.
     if (route.media_source().IsTabMirroringSource() ||
         route.media_source().IsDesktopMirroringSource() ||
         route.media_source().IsLocalFileSource())
       return true;
   } else if (route.controller_type() !=
              media_router::RouteControllerType::kGeneric) {
+    // Hide a route if it doesn't have a generic controller (play, pause etc.).
     return true;
   }
 
+  // Skip the multizone member check if it's a DIAL route.
   if (!route.media_source().IsCastPresentationUrl()) {
     return false;
   }
@@ -83,11 +90,30 @@
 }
 
 std::set<std::string>
-CastMediaNotificationProducer::GetActiveControllableItemIds() {
+CastMediaNotificationProducer::GetActiveControllableItemIds() const {
   std::set<std::string> ids;
   for (const auto& item : items_) {
-    if (item.second.is_active())
-      ids.insert(item.first);
+    if (!item.second.is_active())
+      continue;
+
+// kMediaRouterShowCastSessionsStartedByOtherDevices is not registered on
+// Android nor ChromeOS.
+// // TODO(crbug.com/1308053): Enable it on ChromeOS once Cast+GMC ships.
+#if !BUILDFLAG(IS_CHROMEOS)
+    // The non-local Cast session filter should not be put in
+    // |ShouldHideNotification()| because it's used to determine if an item
+    // should be created. It's possible that users later change the pref to
+    // show all Cast sessions.
+    if (media_router::GlobalMediaControlsCastStartStopEnabled(profile_) &&
+        !this->profile_->GetPrefs()->GetBoolean(
+            media_router::prefs::
+                kMediaRouterShowCastSessionsStartedByOtherDevices) &&
+        !item.second.route_is_local()) {
+      continue;
+    }
+#endif
+
+    ids.insert(item.first);
   }
   return ids;
 }
@@ -170,17 +196,15 @@
 }
 
 size_t CastMediaNotificationProducer::GetActiveItemCount() const {
-  return std::count_if(items_.begin(), items_.end(), [](const auto& item) {
-    return item.second.is_active();
-  });
+  return GetActiveControllableItemIds().size();
 }
 
 bool CastMediaNotificationProducer::HasActiveItems() const {
-  return GetActiveItemCount() != 0;
+  return !GetActiveControllableItemIds().empty();
 }
 
 bool CastMediaNotificationProducer::HasLocalMediaRoute() const {
   return std::find_if(items_.begin(), items_.end(), [](const auto& item) {
-           return item.second.is_local_presentation();
+           return item.second.route_is_local();
          }) != items_.end();
 }
diff --git a/chrome/browser/ui/global_media_controls/cast_media_notification_producer.h b/chrome/browser/ui/global_media_controls/cast_media_notification_producer.h
index fb57b5c4..687f5ca 100644
--- a/chrome/browser/ui/global_media_controls/cast_media_notification_producer.h
+++ b/chrome/browser/ui/global_media_controls/cast_media_notification_producer.h
@@ -48,7 +48,7 @@
   // global_media_controls::MediaItemProducer:
   base::WeakPtr<media_message_center::MediaNotificationItem> GetMediaItem(
       const std::string& id) override;
-  std::set<std::string> GetActiveControllableItemIds() override;
+  std::set<std::string> GetActiveControllableItemIds() const override;
   bool HasFrozenItems() override;
   void OnItemShown(const std::string& id,
                    global_media_controls::MediaItemUI* item_ui) override;
diff --git a/chrome/browser/ui/global_media_controls/cast_media_notification_producer_unittest.cc b/chrome/browser/ui/global_media_controls/cast_media_notification_producer_unittest.cc
index 817d86bf..1ac8de4 100644
--- a/chrome/browser/ui/global_media_controls/cast_media_notification_producer_unittest.cc
+++ b/chrome/browser/ui/global_media_controls/cast_media_notification_producer_unittest.cc
@@ -13,6 +13,8 @@
 #include "components/media_message_center/mock_media_notification_view.h"
 #include "components/media_router/browser/test/mock_media_router.h"
 #include "components/media_router/common/media_route.h"
+#include "components/media_router/common/pref_names.h"
+#include "components/sync_preferences/testing_pref_service_syncable.h"
 #include "content/public/test/browser_task_environment.h"
 #include "testing/gmock/include/gmock/gmock.h"
 #include "testing/gtest/include/gtest/gtest.h"
@@ -51,6 +53,8 @@
 
   void TearDown() override { notification_producer_.reset(); }
 
+  TestingProfile* profile() { return &profile_; }
+
  protected:
   content::BrowserTaskEnvironment task_environment_;
   TestingProfile profile_;
@@ -141,4 +145,21 @@
       {mirroring_route, multizone_member_route, connecting_route});
   EXPECT_EQ(0u, notification_producer_->GetActiveItemCount());
 }
+
+TEST_F(CastMediaNotificationProducerTest, NonLocalRoutesWithoutNotifications) {
+  MediaRoute non_local_route = CreateRoute("non-local-route");
+  non_local_route.set_local(false);
+  sync_preferences::TestingPrefServiceSyncable* pref_service =
+      profile()->GetTestingPrefService();
+
+  notification_producer_->OnRoutesUpdated({non_local_route});
+  EXPECT_EQ(1u, notification_producer_->GetActiveItemCount());
+
+  // There is no need to call |OnRouteUpdated()| here because this is a
+  // client-side change.
+  pref_service->SetBoolean(
+      media_router::prefs::kMediaRouterShowCastSessionsStartedByOtherDevices,
+      false);
+  EXPECT_EQ(0u, notification_producer_->GetActiveItemCount());
+}
 #endif  // BUILDFLAG(IS_CHROMEOS)
diff --git a/chrome/browser/ui/global_media_controls/presentation_request_notification_producer.cc b/chrome/browser/ui/global_media_controls/presentation_request_notification_producer.cc
index 778d85e..422c255 100644
--- a/chrome/browser/ui/global_media_controls/presentation_request_notification_producer.cc
+++ b/chrome/browser/ui/global_media_controls/presentation_request_notification_producer.cc
@@ -100,7 +100,7 @@
 }
 
 std::set<std::string>
-PresentationRequestNotificationProducer::GetActiveControllableItemIds() {
+PresentationRequestNotificationProducer::GetActiveControllableItemIds() const {
   return (item_ && !should_hide_) ? std::set<std::string>({item_->id()})
                                   : std::set<std::string>();
 }
diff --git a/chrome/browser/ui/global_media_controls/presentation_request_notification_producer.h b/chrome/browser/ui/global_media_controls/presentation_request_notification_producer.h
index 6d898f9f..af18401 100644
--- a/chrome/browser/ui/global_media_controls/presentation_request_notification_producer.h
+++ b/chrome/browser/ui/global_media_controls/presentation_request_notification_producer.h
@@ -65,7 +65,7 @@
   base::WeakPtr<media_message_center::MediaNotificationItem> GetMediaItem(
       const std::string& id) override;
   // Returns the supplemental notification's id if it should be shown.
-  std::set<std::string> GetActiveControllableItemIds() override;
+  std::set<std::string> GetActiveControllableItemIds() const override;
   bool HasFrozenItems() override;
   void OnItemShown(const std::string& id,
                    global_media_controls::MediaItemUI* item_ui) override;
diff --git a/chrome/browser/ui/media_router/media_router_ui.cc b/chrome/browser/ui/media_router/media_router_ui.cc
index 76e837a..8eff3c13 100644
--- a/chrome/browser/ui/media_router/media_router_ui.cc
+++ b/chrome/browser/ui/media_router/media_router_ui.cc
@@ -126,8 +126,12 @@
   // If |start_presentation_context_| still exists, then it means presentation
   // route request was never attempted.
   if (start_presentation_context_) {
+    std::vector<MediaSinkWithCastModes> sinks;
+    if (query_result_manager_.get()) {
+      sinks = query_result_manager_->GetSinksWithCastModes();
+    }
     bool presentation_sinks_available = std::any_of(
-        sinks_.begin(), sinks_.end(), [](const MediaSinkWithCastModes& sink) {
+        sinks.begin(), sinks.end(), [](const MediaSinkWithCastModes& sink) {
           return base::Contains(sink.cast_modes, MediaCastMode::PRESENTATION);
         });
     if (presentation_sinks_available) {
@@ -188,11 +192,6 @@
       presentation_manager_->HasDefaultPresentationRequest()) {
     OnDefaultPresentationChanged(
         &presentation_manager_->GetDefaultPresentationRequest());
-  } else {
-    // Register for MediaRoute updates without a media source.
-    routes_observer_ = std::make_unique<UIMediaRoutesObserver>(
-        GetMediaRouter(), base::BindRepeating(&MediaRouterUI::OnRoutesUpdated,
-                                              base::Unretained(this)));
   }
 }
 
@@ -424,6 +423,10 @@
       initiator_,
       base::BindRepeating(&MediaRouterUI::UpdateSinks, base::Unretained(this)));
 
+  routes_observer_ = std::make_unique<UIMediaRoutesObserver>(
+      GetMediaRouter(), base::BindRepeating(&MediaRouterUI::OnRoutesUpdated,
+                                            base::Unretained(this)));
+
   StartObservingIssues();
 }
 
@@ -460,10 +463,6 @@
   query_result_manager_->SetSourcesForCastMode(
       MediaCastMode::PRESENTATION, sources,
       presentation_request_->frame_origin);
-  // Register for MediaRoute updates.
-  routes_observer_ = std::make_unique<UIMediaRoutesObserver>(
-      GetMediaRouter(), base::BindRepeating(&MediaRouterUI::OnRoutesUpdated,
-                                            base::Unretained(this)));
   UpdateModelHeader();
 }
 
@@ -471,11 +470,6 @@
   presentation_request_.reset();
   query_result_manager_->RemoveSourcesForCastMode(MediaCastMode::PRESENTATION);
 
-  // Register for MediaRoute updates.
-  routes_observer_ = std::make_unique<UIMediaRoutesObserver>(
-      GetMediaRouter(), base::BindRepeating(&MediaRouterUI::OnRoutesUpdated,
-                                            base::Unretained(this)));
-
   UpdateModelHeader();
 }
 
diff --git a/chrome/browser/ui/media_router/query_result_manager.cc b/chrome/browser/ui/media_router/query_result_manager.cc
index 28472a9..a6905d3 100644
--- a/chrome/browser/ui/media_router/query_result_manager.cc
+++ b/chrome/browser/ui/media_router/query_result_manager.cc
@@ -159,6 +159,22 @@
                                                   : cast_mode_it->second;
 }
 
+std::vector<MediaSinkWithCastModes> QueryResultManager::GetSinksWithCastModes()
+    const {
+  std::vector<MediaSinkWithCastModes> sinks;
+  for (const auto& sink_pair : sinks_with_sources_) {
+    MediaSinkWithCastModes sink_with_cast_modes(sink_pair.second.sink());
+    sink_with_cast_modes.cast_modes = sink_pair.second.GetCastModes();
+    sinks.push_back(sink_with_cast_modes);
+  }
+  for (const auto& sink : all_sinks_) {
+    if (!base::Contains(sinks_with_sources_, sink.id()))
+      sinks.push_back(MediaSinkWithCastModes(sink));
+  }
+
+  return sinks;
+}
+
 void QueryResultManager::RemoveOldSourcesForCastMode(
     MediaCastMode cast_mode,
     const std::vector<MediaSource>& new_sources) {
@@ -260,16 +276,7 @@
 }
 
 void QueryResultManager::NotifyOnResultsUpdated() {
-  std::vector<MediaSinkWithCastModes> sinks;
-  for (const auto& sink_pair : sinks_with_sources_) {
-    MediaSinkWithCastModes sink_with_cast_modes(sink_pair.second.sink());
-    sink_with_cast_modes.cast_modes = sink_pair.second.GetCastModes();
-    sinks.push_back(sink_with_cast_modes);
-  }
-  for (const auto& sink : all_sinks_) {
-    if (!base::Contains(sinks_with_sources_, sink.id()))
-      sinks.push_back(MediaSinkWithCastModes(sink));
-  }
+  std::vector<MediaSinkWithCastModes> sinks = GetSinksWithCastModes();
   for (QueryResultManager::Observer& observer : observers_)
     observer.OnResultsUpdated(sinks);
 }
diff --git a/chrome/browser/ui/media_router/query_result_manager.h b/chrome/browser/ui/media_router/query_result_manager.h
index 5e473db9..fc49354 100644
--- a/chrome/browser/ui/media_router/query_result_manager.h
+++ b/chrome/browser/ui/media_router/query_result_manager.h
@@ -114,6 +114,9 @@
   // vector if there is none.
   std::vector<MediaSource> GetSourcesForCastMode(MediaCastMode cast_mode) const;
 
+  // Returns all of the currently known sinks with the cast modes they support
+  std::vector<MediaSinkWithCastModes> GetSinksWithCastModes() const;
+
  private:
   class MediaSourceMediaSinksObserver;
   class AnyMediaSinksObserver;
diff --git a/chrome/browser/ui/signin/dice_web_signin_interceptor_delegate.cc b/chrome/browser/ui/signin/dice_web_signin_interceptor_delegate.cc
index 744b4a3..ac4e11a 100644
--- a/chrome/browser/ui/signin/dice_web_signin_interceptor_delegate.cc
+++ b/chrome/browser/ui/signin/dice_web_signin_interceptor_delegate.cc
@@ -17,6 +17,7 @@
 #include "chrome/browser/ui/browser.h"
 #include "chrome/browser/ui/browser_finder.h"
 #include "chrome/browser/ui/browser_window.h"
+#include "chrome/browser/ui/webui/signin/signin_utils.h"
 #include "components/signin/public/base/signin_metrics.h"
 #include "components/signin/public/identity_manager/identity_manager.h"
 #include "google_apis/gaia/core_account_id.h"
@@ -63,19 +64,29 @@
   void ShowEnterpriseProfileInterceptionDialog(const AccountInfo& account_info,
                                                SkColor profile_color) {
     browser_->signin_view_controller()->ShowModalEnterpriseConfirmationDialog(
-        account_info, profile_color,
+        account_info, /*force_new_profile=*/true, profile_color,
         base::BindOnce(&ForcedEnterpriseSigninInterceptionHandle::
                            OnEnterpriseInterceptionDialogClosed,
                        base::Unretained(this)));
   }
 
-  void OnEnterpriseInterceptionDialogClosed(bool create_profile) {
-    if (!create_profile)
-      browser_->signin_view_controller()->CloseModalSignin();
-    std::move(callback_).Run(create_profile
-                                 ? SigninInterceptionResult::kAccepted
-                                 : SigninInterceptionResult::kDeclined);
+  void OnEnterpriseInterceptionDialogClosed(signin::SigninChoice choice) {
+    switch (choice) {
+      case signin::SIGNIN_CHOICE_NEW_PROFILE:
+        std::move(callback_).Run(SigninInterceptionResult::kAccepted);
+        break;
+      case signin::SIGNIN_CHOICE_CANCEL:
+        browser_->signin_view_controller()->CloseModalSignin();
+        std::move(callback_).Run(SigninInterceptionResult::kDeclined);
+        break;
+      case signin::SIGNIN_CHOICE_CONTINUE:
+      case signin::SIGNIN_CHOICE_SIZE:
+      default:
+        NOTREACHED();
+        break;
+    }
   }
+
   raw_ptr<Browser> browser_;
   base::OnceCallback<void(SigninInterceptionResult)> callback_;
 };
diff --git a/chrome/browser/ui/signin_view_controller.cc b/chrome/browser/ui/signin_view_controller.cc
index da8579c4..52f48996 100644
--- a/chrome/browser/ui/signin_view_controller.cc
+++ b/chrome/browser/ui/signin_view_controller.cc
@@ -20,6 +20,7 @@
 #include "chrome/browser/ui/signin_modal_dialog.h"
 #include "chrome/browser/ui/signin_modal_dialog_impl.h"
 #include "chrome/browser/ui/signin_view_controller_delegate.h"
+#include "chrome/browser/ui/webui/signin/signin_utils.h"
 #include "components/signin/public/base/consent_level.h"
 #include "components/signin/public/base/signin_buildflags.h"
 #include "components/signin/public/identity_manager/account_info.h"
@@ -256,18 +257,16 @@
 
 void SigninViewController::ShowModalEnterpriseConfirmationDialog(
     const AccountInfo& account_info,
+    bool force_new_profile,
     SkColor profile_color,
-    base::OnceCallback<void(bool)> callback) {
+    signin::SigninChoiceCallback callback) {
 #if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX) || \
     BUILDFLAG(IS_CHROMEOS_LACROS)
   CloseModalSignin();
   dialog_ = std::make_unique<SigninModalDialogImpl>(
       SigninViewControllerDelegate::CreateEnterpriseConfirmationDelegate(
-          browser_, account_info, profile_color,
-          base::BindOnce(
-              [](Browser* browser, base::OnceCallback<void(bool)> callback,
-                 bool result) { std::move(callback).Run(result); },
-              base::Unretained(browser_), std::move(callback))),
+          browser_, account_info, force_new_profile, profile_color,
+          std::move(callback)),
       GetOnModalDialogClosedCallback());
   chrome::RecordDialogCreation(
       chrome::DialogIdentifier::SIGNIN_ENTERPRISE_INTERCEPTION);
diff --git a/chrome/browser/ui/signin_view_controller.h b/chrome/browser/ui/signin_view_controller.h
index 3549238..25e3fcc 100644
--- a/chrome/browser/ui/signin_view_controller.h
+++ b/chrome/browser/ui/signin_view_controller.h
@@ -14,6 +14,7 @@
 #include "build/build_config.h"
 #include "chrome/browser/ui/profile_chooser_constants.h"
 #include "chrome/browser/ui/signin_modal_dialog.h"
+#include "chrome/browser/ui/webui/signin/signin_utils.h"
 #include "components/signin/public/base/signin_buildflags.h"
 #include "third_party/skia/include/core/SkColor.h"
 #include "url/gurl.h"
@@ -143,8 +144,9 @@
   // on the dialog.
   void ShowModalEnterpriseConfirmationDialog(
       const AccountInfo& account_info,
+      bool force_new_profile,
       SkColor profile_color,
-      base::OnceCallback<void(bool)> callback);
+      signin::SigninChoiceCallback callback);
 
   // Shows the modal sign-in error dialog as a browser-modal dialog on top of
   // the |browser_|'s window.
diff --git a/chrome/browser/ui/signin_view_controller_delegate.h b/chrome/browser/ui/signin_view_controller_delegate.h
index 38860ee7..f3213d5f 100644
--- a/chrome/browser/ui/signin_view_controller_delegate.h
+++ b/chrome/browser/ui/signin_view_controller_delegate.h
@@ -10,6 +10,7 @@
 #include "base/observer_list_types.h"
 #include "build/build_config.h"
 #include "build/chromeos_buildflags.h"
+#include "chrome/browser/ui/webui/signin/signin_utils.h"
 #include "components/signin/public/base/signin_buildflags.h"
 #include "third_party/skia/include/core/SkColor.h"
 
@@ -81,8 +82,9 @@
   static SigninViewControllerDelegate* CreateEnterpriseConfirmationDelegate(
       Browser* browser,
       const AccountInfo& account_info,
+      bool force_new_profile,
       SkColor profile_color,
-      base::OnceCallback<void(bool)> callback);
+      signin::SigninChoiceCallback callback);
 #endif
 
   void AddObserver(Observer* observer);
diff --git a/chrome/browser/ui/signin_view_controller_interactive_uitest.cc b/chrome/browser/ui/signin_view_controller_interactive_uitest.cc
index c11a1bcd..92f02704 100644
--- a/chrome/browser/ui/signin_view_controller_interactive_uitest.cc
+++ b/chrome/browser/ui/signin_view_controller_interactive_uitest.cc
@@ -205,13 +205,14 @@
   content::TestNavigationObserver content_observer(
       GURL("chrome://enterprise-profile-welcome/"));
   content_observer.StartWatchingNewWebContents();
-  bool result;
+  signin::SigninChoice result;
   browser()->signin_view_controller()->ShowModalEnterpriseConfirmationDialog(
-      account_info, SK_ColorWHITE,
+      account_info, /*force_new_profile=*/true, SK_ColorWHITE,
       base::BindOnce(
-          [](Browser* browser, bool* result, bool create) {
+          [](Browser* browser, signin::SigninChoice* result,
+             signin::SigninChoice choice) {
             browser->signin_view_controller()->CloseModalSignin();
-            *result = create;
+            *result = choice;
           },
           browser(), &result));
   EXPECT_TRUE(browser()->signin_view_controller()->ShowsModalDialog());
@@ -227,6 +228,6 @@
                                               /*command=*/false));
 
   dialog_destroyed_watcher.Wait();
-  EXPECT_TRUE(result);
+  EXPECT_EQ(result, signin::SigninChoice::SIGNIN_CHOICE_NEW_PROFILE);
   EXPECT_FALSE(browser()->signin_view_controller()->ShowsModalDialog());
 }
diff --git a/chrome/browser/ui/task_manager/task_manager_table_model.cc b/chrome/browser/ui/task_manager/task_manager_table_model.cc
index e2f4d0c..710bee3 100644
--- a/chrome/browser/ui/task_manager/task_manager_table_model.cc
+++ b/chrome/browser/ui/task_manager/task_manager_table_model.cc
@@ -6,6 +6,9 @@
 
 #include <stddef.h>
 
+#include <string>
+#include <vector>
+
 #include "base/command_line.h"
 #include "base/i18n/number_formatting.h"
 #include "base/i18n/rtl.h"
@@ -826,9 +829,9 @@
   // Do a best effort of retrieving the correct settings from the local state.
   // Use the default settings of the value if it fails to be retrieved.
   const std::string* sorted_col_id =
-      dictionary->FindStringKey(kSortColumnIdKey);
+      dictionary->GetDict().FindString(kSortColumnIdKey);
   bool sort_is_ascending =
-      dictionary->FindBoolKey(kSortIsAscendingKey).value_or(true);
+      dictionary->GetDict().FindBool(kSortIsAscendingKey).value_or(true);
 
   int current_visible_column_index = 0;
   for (size_t i = 0; i < kColumnsSize; ++i) {
@@ -874,13 +877,14 @@
 
   // Store the current sort status to be restored again at startup.
   if (!table_view_delegate_->IsTableSorted()) {
-    dict_update->SetStringKey(kSortColumnIdKey, "");
+    dict_update->GetDict().Set(kSortColumnIdKey, "");
   } else {
     const auto& sort_descriptor = table_view_delegate_->GetSortDescriptor();
-    dict_update->SetStringKey(
+    dict_update->GetDict().Set(
         kSortColumnIdKey,
         GetColumnIdAsString(sort_descriptor.sorted_column_id));
-    dict_update->SetBoolKey(kSortIsAscendingKey, sort_descriptor.is_ascending);
+    dict_update->GetDict().Set(kSortIsAscendingKey,
+                               sort_descriptor.is_ascending);
   }
 }
 
diff --git a/chrome/browser/ui/views/download/download_item_view.cc b/chrome/browser/ui/views/download/download_item_view.cc
index 118f311..8677c236 100644
--- a/chrome/browser/ui/views/download/download_item_view.cc
+++ b/chrome/browser/ui/views/download/download_item_view.cc
@@ -1061,8 +1061,6 @@
     const gfx::RectF& bounds,
     const base::TimeDelta& indeterminate_progress_time,
     int percent_done) const {
-  const SkColor color = GetColorProvider()->GetColor(ui::kColorThrobber);
-
   // Calculate progress.
   SkScalar start_pos = SkIntToScalar(270);  // 12 o'clock
   SkScalar sweep_angle = SkDoubleToScalar(360 * percent_done / 100.0);
@@ -1074,9 +1072,12 @@
     sweep_angle = SkIntToScalar(50);
   }
 
-  views::DrawProgressRing(canvas, gfx::RectFToSkRect(bounds),
-                          SkColorSetA(color, 0x33), color,
-                          /*stroke_width=*/1.7f, start_pos, sweep_angle);
+  const auto* color_provider = GetColorProvider();
+  views::DrawProgressRing(
+      canvas, gfx::RectFToSkRect(bounds),
+      color_provider->GetColor(kColorDownloadItemProgressRingBackground),
+      color_provider->GetColor(kColorDownloadItemProgressRingForeground),
+      /*stroke_width=*/1.7f, start_pos, sweep_angle);
 }
 
 ui::ImageModel DownloadItemView::GetIcon() const {
diff --git a/chrome/browser/ui/views/global_media_controls/media_toolbar_button_contextual_menu.cc b/chrome/browser/ui/views/global_media_controls/media_toolbar_button_contextual_menu.cc
index 2c817c8..f093a6f 100644
--- a/chrome/browser/ui/views/global_media_controls/media_toolbar_button_contextual_menu.cc
+++ b/chrome/browser/ui/views/global_media_controls/media_toolbar_button_contextual_menu.cc
@@ -9,37 +9,47 @@
 #include "chrome/browser/media/router/media_router_feature.h"
 #include "chrome/browser/profiles/profile.h"
 #include "chrome/browser/ui/browser.h"
+#include "chrome/browser/ui/global_media_controls/media_notification_service.h"
+#include "chrome/browser/ui/global_media_controls/media_notification_service_factory.h"
 #include "chrome/browser/ui/singleton_tabs.h"
 #include "chrome/common/pref_names.h"
 #include "chrome/common/url_constants.h"
 #include "chrome/grit/generated_resources.h"
+#include "components/global_media_controls/public/media_item_manager.h"
+#include "components/media_router/common/pref_names.h"
 #include "components/prefs/pref_service.h"
 
+namespace {
+global_media_controls::MediaItemManager* GetItemManagerFromBrowser(
+    Browser* browser) {
+  return MediaNotificationServiceFactory::GetForProfile(browser->profile())
+      ->media_item_manager();
+}
+}  // namespace
+
 std::unique_ptr<MediaToolbarButtonContextualMenu>
 MediaToolbarButtonContextualMenu::Create(Browser* browser) {
-#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
   if (media_router::GlobalMediaControlsCastStartStopEnabled(
           browser->profile())) {
     return std::make_unique<MediaToolbarButtonContextualMenu>(browser);
   }
-#endif
   return nullptr;
 }
 
 MediaToolbarButtonContextualMenu::MediaToolbarButtonContextualMenu(
     Browser* browser)
-#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
-    : browser_(browser)
-#endif
-{
-}
+    : browser_(browser), item_manager_(GetItemManagerFromBrowser(browser_)) {}
 
 MediaToolbarButtonContextualMenu::~MediaToolbarButtonContextualMenu() = default;
 
 std::unique_ptr<ui::SimpleMenuModel>
 MediaToolbarButtonContextualMenu::CreateMenuModel() {
-#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
   auto menu_model = std::make_unique<ui::SimpleMenuModel>(this);
+  menu_model->AddCheckItemWithStringId(
+      IDC_MEDIA_TOOLBAR_CONTEXT_SHOW_OTHER_SESSIONS,
+      IDS_MEDIA_TOOLBAR_CONTEXT_SHOW_OTHER_SESSIONS);
+
+#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
   if (!browser_->profile()->IsOffTheRecord() &&
       browser_->profile()->GetPrefs()->GetBoolean(
           prefs::kUserFeedbackAllowed)) {
@@ -47,15 +57,29 @@
         IDC_MEDIA_TOOLBAR_CONTEXT_REPORT_CAST_ISSUE,
         IDS_MEDIA_TOOLBAR_CONTEXT_REPORT_CAST_ISSUE);
   }
-  return menu_model;
-#else
-  return nullptr;
 #endif
+  return menu_model;
+}
+
+bool MediaToolbarButtonContextualMenu::IsCommandIdChecked(
+    int command_id) const {
+  PrefService* pref_service = browser_->profile()->GetPrefs();
+  switch (command_id) {
+    case IDC_MEDIA_TOOLBAR_CONTEXT_SHOW_OTHER_SESSIONS:
+      return pref_service->GetBoolean(
+          media_router::prefs::
+              kMediaRouterShowCastSessionsStartedByOtherDevices);
+    default:
+      return false;
+  }
 }
 
 void MediaToolbarButtonContextualMenu::ExecuteCommand(int command_id,
                                                       int event_flags) {
   switch (command_id) {
+    case IDC_MEDIA_TOOLBAR_CONTEXT_SHOW_OTHER_SESSIONS:
+      ToggleShowOtherSessions();
+      break;
 #if BUILDFLAG(GOOGLE_CHROME_BRANDING)
     case IDC_MEDIA_TOOLBAR_CONTEXT_REPORT_CAST_ISSUE:
       ReportIssue();
@@ -66,6 +90,21 @@
   }
 }
 
+void MediaToolbarButtonContextualMenu::MenuClosed(ui::SimpleMenuModel* source) {
+  if (item_manager_) {
+    item_manager_->OnItemsChanged();
+  }
+}
+
+void MediaToolbarButtonContextualMenu::ToggleShowOtherSessions() {
+  PrefService* pref_service = browser_->profile()->GetPrefs();
+  pref_service->SetBoolean(
+      media_router::prefs::kMediaRouterShowCastSessionsStartedByOtherDevices,
+      !pref_service->GetBoolean(
+          media_router::prefs::
+              kMediaRouterShowCastSessionsStartedByOtherDevices));
+}
+
 #if BUILDFLAG(GOOGLE_CHROME_BRANDING)
 void MediaToolbarButtonContextualMenu::ReportIssue() {
   ShowSingletonTab(
diff --git a/chrome/browser/ui/views/global_media_controls/media_toolbar_button_contextual_menu.h b/chrome/browser/ui/views/global_media_controls/media_toolbar_button_contextual_menu.h
index bfc9844..384643e 100644
--- a/chrome/browser/ui/views/global_media_controls/media_toolbar_button_contextual_menu.h
+++ b/chrome/browser/ui/views/global_media_controls/media_toolbar_button_contextual_menu.h
@@ -10,11 +10,13 @@
 #include "ui/base/models/simple_menu_model.h"
 
 class Browser;
+namespace global_media_controls {
+class MediaItemManager;
+}
 
-// The contextual menu of the media toolbar button has only one item, which is
-// to open the Cast feedback page. So this class should be instantiated when:
-//  (1) It is a Chrome branded build.
-//  (2) GlobalMediaControlsCastStartStop is enabled.
+// The contextual menu of the media toolbar button has two items, both of which
+// are related to Cast. So this class should be instantiated only when
+// GlobalMediaControlsCastStartStop is enabled.
 class MediaToolbarButtonContextualMenu : public ui::SimpleMenuModel::Delegate {
  public:
   static std::unique_ptr<MediaToolbarButtonContextualMenu> Create(
@@ -32,13 +34,18 @@
   friend class MediaToolbarButtonContextualMenuTest;
 
   // ui::SimpleMenuModel::Delegate:
+  bool IsCommandIdChecked(int command_id) const override;
   void ExecuteCommand(int command_id, int event_flags) override;
+  void MenuClosed(ui::SimpleMenuModel* source) override;
+
+  void ToggleShowOtherSessions();
 
 #if BUILDFLAG(GOOGLE_CHROME_BRANDING)
   // Opens the Cast feedback page.
   void ReportIssue();
+#endif  // BUILDFLAG(GOOGLE_CHROME_BRANDING)
 
   const raw_ptr<Browser> browser_;
-#endif  // BUILDFLAG(GOOGLE_CHROME_BRANDING)
+  const raw_ptr<global_media_controls::MediaItemManager> item_manager_;
 };
 #endif  // CHROME_BROWSER_UI_VIEWS_GLOBAL_MEDIA_CONTROLS_MEDIA_TOOLBAR_BUTTON_CONTEXTUAL_MENU_H_
diff --git a/chrome/browser/ui/views/global_media_controls/media_toolbar_button_contextual_menu_unittest.cc b/chrome/browser/ui/views/global_media_controls/media_toolbar_button_contextual_menu_unittest.cc
index e7d68925..68d1d33 100644
--- a/chrome/browser/ui/views/global_media_controls/media_toolbar_button_contextual_menu_unittest.cc
+++ b/chrome/browser/ui/views/global_media_controls/media_toolbar_button_contextual_menu_unittest.cc
@@ -12,6 +12,7 @@
 #include "chrome/test/base/browser_with_test_window_test.h"
 #include "chrome/test/base/menu_model_test.h"
 #include "components/media_router/browser/test/mock_media_router.h"
+#include "components/media_router/common/pref_names.h"
 
 class MediaToolbarButtonContextualMenuTest : public MenuModelTest,
                                              public BrowserWithTestWindowTest {
@@ -29,6 +30,15 @@
     menu_ = MediaToolbarButtonContextualMenu::Create(browser());
   }
 
+  void ExecuteToggleOtherSessionCommand() {
+    menu_->ExecuteCommand(IDC_MEDIA_TOOLBAR_CONTEXT_SHOW_OTHER_SESSIONS, 0);
+  }
+
+  bool IsOtherSessionItemChecked() {
+    return menu_->IsCommandIdChecked(
+        IDC_MEDIA_TOOLBAR_CONTEXT_SHOW_OTHER_SESSIONS);
+  }
+
 #if BUILDFLAG(GOOGLE_CHROME_BRANDING)
   void ExecuteReportIssueCommand() {
     menu_->ExecuteCommand(IDC_MEDIA_TOOLBAR_CONTEXT_REPORT_CAST_ISSUE, 0);
@@ -40,22 +50,44 @@
   base::test::ScopedFeatureList feature_list_;
 };
 
-#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
 TEST_F(MediaToolbarButtonContextualMenuTest, ShowMenu) {
   auto menu = MediaToolbarButtonContextualMenu::Create(browser());
   auto model = menu->CreateMenuModel();
-  EXPECT_EQ(model->GetItemCount(), 1);
-  EXPECT_EQ(model->GetCommandIdAt(0),
+#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
+  EXPECT_EQ(model->GetItemCount(), 2);
+  EXPECT_EQ(model->GetCommandIdAt(1),
             IDC_MEDIA_TOOLBAR_CONTEXT_REPORT_CAST_ISSUE);
-}
 #else
-TEST_F(MediaToolbarButtonContextualMenuTest, DoNotShowMenu) {
-  EXPECT_FALSE(MediaToolbarButtonContextualMenu::Create(browser()));
+  EXPECT_EQ(model->GetItemCount(), 1);
+#endif
+  EXPECT_EQ(model->GetCommandIdAt(0),
+            IDC_MEDIA_TOOLBAR_CONTEXT_SHOW_OTHER_SESSIONS);
+}
+
+// The kMediaRouterShowCastSessionsStartedByOtherDevices pref is not registered
+// on ChromeOS nor Android.
+#if !BUILDFLAG(IS_CHROMEOS) && !BUILDFLAG(IS_ANDROID)
+TEST_F(MediaToolbarButtonContextualMenuTest, ToggleOtherSessionsItem) {
+  PrefService* pref_service = browser()->profile()->GetPrefs();
+  pref_service->SetBoolean(
+      media_router::prefs::kMediaRouterShowCastSessionsStartedByOtherDevices,
+      false);
+  EXPECT_FALSE(IsOtherSessionItemChecked());
+
+  ExecuteToggleOtherSessionCommand();
+  EXPECT_TRUE(IsOtherSessionItemChecked());
+  EXPECT_TRUE(pref_service->GetBoolean(
+      media_router::prefs::kMediaRouterShowCastSessionsStartedByOtherDevices));
+
+  ExecuteToggleOtherSessionCommand();
+  EXPECT_FALSE(IsOtherSessionItemChecked());
+  EXPECT_FALSE(pref_service->GetBoolean(
+      media_router::prefs::kMediaRouterShowCastSessionsStartedByOtherDevices));
 }
 #endif
 
 #if BUILDFLAG(GOOGLE_CHROME_BRANDING)
-TEST_F(MediaToolbarButtonContextualMenuTest, ExecuteCommand) {
+TEST_F(MediaToolbarButtonContextualMenuTest, ExecuteReportIssueCommand) {
   ExecuteReportIssueCommand();
   EXPECT_EQ(browser()->tab_strip_model()->GetWebContentsAt(0)->GetURL(),
             GURL("chrome://cast-feedback"));
diff --git a/chrome/browser/ui/views/menu_item_view_interactive_uitest.cc b/chrome/browser/ui/views/menu_item_view_interactive_uitest.cc
index 5d024d3..fc759e5 100644
--- a/chrome/browser/ui/views/menu_item_view_interactive_uitest.cc
+++ b/chrome/browser/ui/views/menu_item_view_interactive_uitest.cc
@@ -58,15 +58,7 @@
 // If this flakes, disable and log details in http://crbug.com/523255.
 VIEW_TEST(MenuItemViewTestBasic0, SelectItem0)
 VIEW_TEST(MenuItemViewTestBasic1, SelectItem1)
-
-// If this flakes, disable and log details in http://crbug.com/523255.
-// Flake on Linux Tests (Wayland) builder. see http://crbug.com/523255.
-#if BUILDFLAG(IS_LINUX)
-#define MAYBE_SelectItem2 DISABLED_SelectItem2
-#else
-#define MAYBE_SelectItem2 SelectItem2
-#endif
-VIEW_TEST(MenuItemViewTestBasic2, MAYBE_SelectItem2)
+VIEW_TEST(MenuItemViewTestBasic2, SelectItem2)
 
 // Test class for inserting a menu item while the menu is open.
 template <int INSERT_INDEX, int SELECT_INDEX>
diff --git a/chrome/browser/ui/views/profiles/profile_picker_signed_in_flow_controller.cc b/chrome/browser/ui/views/profiles/profile_picker_signed_in_flow_controller.cc
index 69beea1..c59e8f2 100644
--- a/chrome/browser/ui/views/profiles/profile_picker_signed_in_flow_controller.cc
+++ b/chrome/browser/ui/views/profiles/profile_picker_signed_in_flow_controller.cc
@@ -78,7 +78,7 @@
 
 void ProfilePickerSignedInFlowController::SwitchToEnterpriseProfileWelcome(
     EnterpriseProfileWelcomeUI::ScreenType type,
-    base::OnceCallback<void(bool)> proceed_callback) {
+    signin::SigninChoiceCallback proceed_callback) {
   DCHECK(IsInitialized());
   host_->ShowScreen(contents(),
                     GURL(chrome::kChromeUIEnterpriseProfileWelcomeURL),
@@ -150,7 +150,7 @@
 void ProfilePickerSignedInFlowController::
     SwitchToEnterpriseProfileWelcomeFinished(
         EnterpriseProfileWelcomeUI::ScreenType type,
-        base::OnceCallback<void(bool)> proceed_callback) {
+        signin::SigninChoiceCallback proceed_callback) {
   DCHECK(IsInitialized());
   // Initialize the WebUI page once we know it's committed.
   EnterpriseProfileWelcomeUI* enterprise_profile_welcome_ui =
@@ -163,7 +163,8 @@
       /*browser=*/nullptr, type,
       IdentityManagerFactory::GetForProfile(profile_)
           ->FindExtendedAccountInfoByEmailAddress(email_),
-      GetProfileColor(), std::move(proceed_callback));
+      /*force_new_profile_=*/true, GetProfileColor(),
+      std::move(proceed_callback));
 }
 
 bool ProfilePickerSignedInFlowController::IsInitialized() const {
diff --git a/chrome/browser/ui/views/profiles/profile_picker_signed_in_flow_controller.h b/chrome/browser/ui/views/profiles/profile_picker_signed_in_flow_controller.h
index 0a6b50df..33c0b06 100644
--- a/chrome/browser/ui/views/profiles/profile_picker_signed_in_flow_controller.h
+++ b/chrome/browser/ui/views/profiles/profile_picker_signed_in_flow_controller.h
@@ -61,7 +61,7 @@
   // screen.
   void SwitchToEnterpriseProfileWelcome(
       EnterpriseProfileWelcomeUI::ScreenType type,
-      base::OnceCallback<void(bool)> proceed_callback);
+      signin::SigninChoiceCallback proceed_callback);
 
   // When the sign-in flow cannot be completed because another profile at
   // `profile_path` is already syncing with a chosen account, shows the profile
@@ -94,7 +94,7 @@
   void SwitchToSyncConfirmationFinished();
   void SwitchToEnterpriseProfileWelcomeFinished(
       EnterpriseProfileWelcomeUI::ScreenType type,
-      base::OnceCallback<void(bool)> proceed_callback);
+      signin::SigninChoiceCallback proceed_callback);
 
   // Returns whether the flow is initialized (i.e. whether `Init()` has been
   // called).
diff --git a/chrome/browser/ui/views/profiles/profile_picker_turn_sync_on_delegate.cc b/chrome/browser/ui/views/profiles/profile_picker_turn_sync_on_delegate.cc
index 468d38482..df6e5c4 100644
--- a/chrome/browser/ui/views/profiles/profile_picker_turn_sync_on_delegate.cc
+++ b/chrome/browser/ui/views/profiles/profile_picker_turn_sync_on_delegate.cc
@@ -224,8 +224,8 @@
 
 void ProfilePickerTurnSyncOnDelegate::OnEnterpriseWelcomeClosed(
     EnterpriseProfileWelcomeUI::ScreenType type,
-    bool proceed) {
-  if (!proceed) {
+    signin::SigninChoice choice) {
+  if (choice == signin::SIGNIN_CHOICE_CANCEL) {
     LogOutcome(ProfileMetrics::ProfileSignedInFlowOutcome::
                    kAbortedOnEnterpriseWelcome);
     // The callback provided by TurnSyncOnHelper must be called, UI_CLOSED
@@ -237,6 +237,8 @@
     return;
   }
 
+  DCHECK_EQ(choice, signin::SIGNIN_CHOICE_NEW_PROFILE);
+
   switch (type) {
     case EnterpriseProfileWelcomeUI::ScreenType::kEntepriseAccountSyncEnabled:
       ShowSyncConfirmationScreen();
diff --git a/chrome/browser/ui/views/profiles/profile_picker_turn_sync_on_delegate.h b/chrome/browser/ui/views/profiles/profile_picker_turn_sync_on_delegate.h
index 8c62234..98d24018 100644
--- a/chrome/browser/ui/views/profiles/profile_picker_turn_sync_on_delegate.h
+++ b/chrome/browser/ui/views/profiles/profile_picker_turn_sync_on_delegate.h
@@ -67,7 +67,7 @@
   // Shows the enterprise welcome screen.
   void ShowEnterpriseWelcome(EnterpriseProfileWelcomeUI::ScreenType type);
   void OnEnterpriseWelcomeClosed(EnterpriseProfileWelcomeUI::ScreenType type,
-                                 bool proceed);
+                                 signin::SigninChoice choice);
 
   // Reports metric with the outcome of the turn-sync-on flow.
   void LogOutcome(ProfileMetrics::ProfileSignedInFlowOutcome outcome);
diff --git a/chrome/browser/ui/views/profiles/profile_picker_view_browsertest.cc b/chrome/browser/ui/views/profiles/profile_picker_view_browsertest.cc
index 3700e7f..3e502f9d 100644
--- a/chrome/browser/ui/views/profiles/profile_picker_view_browsertest.cc
+++ b/chrome/browser/ui/views/profiles/profile_picker_view_browsertest.cc
@@ -53,6 +53,7 @@
 #include "chrome/browser/ui/webui/signin/profile_picker_handler.h"
 #include "chrome/browser/ui/webui/signin/profile_picker_ui.h"
 #include "chrome/browser/ui/webui/signin/signin_url_utils.h"
+#include "chrome/browser/ui/webui/signin/signin_utils.h"
 #include "chrome/common/pref_names.h"
 #include "chrome/test/base/in_process_browser_test.h"
 #include "chrome/test/base/profile_deletion_observer.h"
@@ -1270,7 +1271,7 @@
 
   void ExpectEnterpriseScreenTypeAndProceed(
       EnterpriseProfileWelcomeUI::ScreenType expected_type,
-      bool proceed) {
+      signin::SigninChoice choice) {
     EnterpriseProfileWelcomeHandler* handler =
         web_contents()
             ->GetWebUI()
@@ -1280,7 +1281,7 @@
     EXPECT_EQ(handler->GetTypeForTesting(), expected_type);
 
     // Simulate clicking on the next button.
-    handler->CallProceedCallbackForTesting(proceed);
+    handler->CallProceedCallbackForTesting(choice);
   }
 };
 
@@ -1299,7 +1300,7 @@
   ExpectEnterpriseScreenTypeAndProceed(
       /*expected_type=*/EnterpriseProfileWelcomeUI::ScreenType::
           kEntepriseAccountSyncEnabled,
-      /*proceed=*/true);
+      /*choice=*/signin::SIGNIN_CHOICE_NEW_PROFILE);
 
   WaitForLoadStop(GetSyncConfirmationURL());
   // Simulate finishing the flow with "No, thanks".
@@ -1356,7 +1357,7 @@
   ExpectEnterpriseScreenTypeAndProceed(
       /*expected_type=*/EnterpriseProfileWelcomeUI::ScreenType::
           kConsumerAccountSyncDisabled,
-      /*proceed=*/true);
+      /*choice=*/signin::SIGNIN_CHOICE_NEW_PROFILE);
 
   Browser* new_browser = BrowserAddedWaiter(2u).Wait();
   WaitForLoadStop(GURL("chrome://newtab/"),
@@ -1400,7 +1401,7 @@
   ExpectEnterpriseScreenTypeAndProceed(
       /*expected_type=*/EnterpriseProfileWelcomeUI::ScreenType::
           kEntepriseAccountSyncEnabled,
-      /*proceed=*/true);
+      /*choice=*/signin::SIGNIN_CHOICE_NEW_PROFILE);
 
   WaitForLoadStop(GetSyncConfirmationURL());
   // Simulate finishing the flow with "Configure sync".
@@ -1454,7 +1455,7 @@
   ExpectEnterpriseScreenTypeAndProceed(
       /*expected_type=*/EnterpriseProfileWelcomeUI::ScreenType::
           kEntepriseAccountSyncEnabled,
-      /*proceed=*/false);
+      /*choice=*/signin::SIGNIN_CHOICE_CANCEL);
 
   // As the profile creation flow was opened directly, the window is closed now.
   WaitForPickerClosed();
diff --git a/chrome/browser/ui/views/profiles/signin_view_controller_delegate_views.cc b/chrome/browser/ui/views/profiles/signin_view_controller_delegate_views.cc
index ee8d372..22b29eb 100644
--- a/chrome/browser/ui/views/profiles/signin_view_controller_delegate_views.cc
+++ b/chrome/browser/ui/views/profiles/signin_view_controller_delegate_views.cc
@@ -19,6 +19,7 @@
 #include "chrome/browser/ui/views/frame/browser_view.h"
 #include "chrome/browser/ui/webui/signin/profile_customization_ui.h"
 #include "chrome/browser/ui/webui/signin/signin_url_utils.h"
+#include "chrome/browser/ui/webui/signin/signin_utils.h"
 #include "chrome/browser/ui/webui/signin/sync_confirmation_ui.h"
 #include "chrome/common/url_constants.h"
 #include "chrome/common/webui_url_constants.h"
@@ -143,8 +144,9 @@
 SigninViewControllerDelegateViews::CreateEnterpriseConfirmationWebView(
     Browser* browser,
     const AccountInfo& account_info,
+    bool force_new_profile,
     SkColor profile_color,
-    base::OnceCallback<void(bool)> callback) {
+    signin::SigninChoiceCallback callback) {
   std::unique_ptr<views::WebView> web_view = CreateDialogWebView(
       browser, GURL(chrome::kChromeUIEnterpriseProfileWelcomeURL),
       kSyncConfirmationDialogHeight, kSyncConfirmationDialogWidth,
@@ -159,7 +161,7 @@
   web_dialog_ui->Initialize(
       browser,
       EnterpriseProfileWelcomeUI::ScreenType::kEnterpriseAccountCreation,
-      account_info, profile_color, std::move(callback));
+      account_info, force_new_profile, profile_color, std::move(callback));
 
   return web_view;
 }
@@ -420,11 +422,13 @@
 SigninViewControllerDelegate::CreateEnterpriseConfirmationDelegate(
     Browser* browser,
     const AccountInfo& account_info,
+    bool force_new_profile,
     SkColor profile_color,
-    base::OnceCallback<void(bool)> callback) {
+    signin::SigninChoiceCallback callback) {
   return new SigninViewControllerDelegateViews(
       SigninViewControllerDelegateViews::CreateEnterpriseConfirmationWebView(
-          browser, account_info, profile_color, std::move(callback)),
+          browser, account_info, force_new_profile, profile_color,
+          std::move(callback)),
       browser, ui::MODAL_TYPE_WINDOW, true, false);
 }
 #endif
diff --git a/chrome/browser/ui/views/profiles/signin_view_controller_delegate_views.h b/chrome/browser/ui/views/profiles/signin_view_controller_delegate_views.h
index 99583697..4e21a7f 100644
--- a/chrome/browser/ui/views/profiles/signin_view_controller_delegate_views.h
+++ b/chrome/browser/ui/views/profiles/signin_view_controller_delegate_views.h
@@ -14,6 +14,7 @@
 #include "chrome/browser/ui/profile_chooser_constants.h"
 #include "chrome/browser/ui/signin_view_controller_delegate.h"
 #include "chrome/browser/ui/webui/signin/enterprise_profile_welcome_ui.h"
+#include "chrome/browser/ui/webui/signin/signin_utils.h"
 #include "components/signin/public/base/signin_buildflags.h"
 #include "content/public/browser/web_contents_delegate.h"
 #include "third_party/skia/include/core/SkColor.h"
@@ -75,8 +76,9 @@
   static std::unique_ptr<views::WebView> CreateEnterpriseConfirmationWebView(
       Browser* browser,
       const AccountInfo& account_info,
+      bool force_new_profile,
       SkColor profile_color,
-      base::OnceCallback<void(bool)> callback);
+      signin::SigninChoiceCallback callback);
 #endif
 
   // views::DialogDelegateView:
diff --git a/chrome/browser/ui/views/web_apps/force_installed_deprecated_apps_dialog_view.cc b/chrome/browser/ui/views/web_apps/force_installed_deprecated_apps_dialog_view.cc
index 72602e3..57dd9109 100644
--- a/chrome/browser/ui/views/web_apps/force_installed_deprecated_apps_dialog_view.cc
+++ b/chrome/browser/ui/views/web_apps/force_installed_deprecated_apps_dialog_view.cc
@@ -14,6 +14,7 @@
 #include "ui/base/window_open_disposition.h"
 #include "ui/views/controls/link.h"
 #include "ui/views/controls/styled_label.h"
+#include "ui/views/layout/box_layout.h"
 #include "ui/views/layout/layout_provider.h"
 #include "ui/views/window/dialog_delegate.h"
 
@@ -47,34 +48,31 @@
   const extensions::Extension* extension =
       extensions::ExtensionRegistry::Get(browser_context)
           ->GetInstalledExtension(app_id_);
-  SetUseDefaultFillLayout(true);
-  auto* info_label = AddChildView(std::make_unique<views::StyledLabel>());
+  SetLayoutManager(std::make_unique<views::BoxLayout>(
+      views::BoxLayout::Orientation::kVertical, gfx::Insets(),
+      ChromeLayoutProvider::Get()->GetDistanceMetric(
+          views::DISTANCE_RELATED_CONTROL_VERTICAL)));
+  auto* info_label = AddChildView(std::make_unique<views::Label>(
+      l10n_util::GetStringFUTF16(IDS_FORCE_INSTALLED_DEPRECATED_APPS_CONTENT,
+                                 base::UTF8ToUTF16(extension->name()))));
+  info_label->SetMultiLine(true);
+  info_label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
 
-  std::vector<size_t> offsets;
-  std::u16string link_text =
-      l10n_util::GetStringUTF16(IDS_DEPRECATED_APPS_LEARN_MORE);
-  std::u16string info_text =
-      l10n_util::GetStringUTF16(IDS_FORCE_INSTALLED_DEPRECATED_APPS_CONTENT);
-  std::u16string label_text = l10n_util::FormatString(
-      info_text, {base::UTF8ToUTF16(extension->name()), link_text}, &offsets);
-
-  const size_t offset = offsets.back();
-
-  auto link_style =
-      views::StyledLabel::RangeStyleInfo::CreateForLink(base::BindRepeating(
-          [](content::WebContents* web_contents, const ui::Event& event) {
-            web_contents->OpenURL(content::OpenURLParams(
-                GURL(chrome::kChromeAppsDeprecationLearnMoreURL),
-                content::Referrer(),
-                ui::DispositionFromEventFlags(
-                    event.flags(), WindowOpenDisposition::NEW_FOREGROUND_TAB),
-                ui::PAGE_TRANSITION_LINK, /*is_renderer_initiated=*/false));
-          },
-          web_contents_));
-  link_style.disable_line_wrapping = true;
-  info_label->SetText(label_text);
-  info_label->AddStyleRange(gfx::Range(offset, offset + link_text.length()),
-                            link_style);
+  auto* learn_more = AddChildView(std::make_unique<views::Link>(
+      l10n_util::GetStringUTF16(IDS_DEPRECATED_APPS_LEARN_MORE)));
+  learn_more->SetCallback(base::BindRepeating(
+      [](content::WebContents* web_contents, const ui::Event& event) {
+        web_contents->OpenURL(content::OpenURLParams(
+            GURL(chrome::kChromeAppsDeprecationLearnMoreURL),
+            content::Referrer(),
+            ui::DispositionFromEventFlags(
+                event.flags(), WindowOpenDisposition::NEW_FOREGROUND_TAB),
+            ui::PAGE_TRANSITION_LINK, /*is_renderer_initiated=*/false));
+      },
+      web_contents_));
+  learn_more->SetAccessibleName(l10n_util::GetStringUTF16(
+      IDS_FORCE_INSTALLED_DEPRECATED_APPS_LEARN_MORE_AX_LABEL));
+  learn_more->SetHorizontalAlignment(gfx::ALIGN_LEFT);
 }
 
 BEGIN_METADATA(ForceInstalledDeprecatedAppsDialogView, views::View)
diff --git a/chrome/browser/ui/webui/access_code_cast/access_code_cast_handler.cc b/chrome/browser/ui/webui/access_code_cast/access_code_cast_handler.cc
index d351b0e..6bcd801 100644
--- a/chrome/browser/ui/webui/access_code_cast/access_code_cast_handler.cc
+++ b/chrome/browser/ui/webui/access_code_cast/access_code_cast_handler.cc
@@ -153,8 +153,15 @@
   // If |start_presentation_context_| still exists, then it means presentation
   // route request was never attempted.
   if (start_presentation_context_) {
-    if (sink_id_ &&
-        base::Contains(supported_cast_modes_, MediaCastMode::PRESENTATION)) {
+    std::vector<MediaSinkWithCastModes> sinks;
+    if (query_result_manager_.get()) {
+      sinks = query_result_manager_->GetSinksWithCastModes();
+    }
+    bool presentation_sinks_available = std::any_of(
+        sinks.begin(), sinks.end(), [](const MediaSinkWithCastModes& sink) {
+          return base::Contains(sink.cast_modes, MediaCastMode::PRESENTATION);
+        });
+    if (presentation_sinks_available) {
       start_presentation_context_->InvokeErrorCallback(
           blink::mojom::PresentationError(blink::mojom::PresentationErrorType::
                                               PRESENTATION_REQUEST_CANCELLED,
diff --git a/chrome/browser/ui/webui/access_code_cast/access_code_cast_handler.h b/chrome/browser/ui/webui/access_code_cast/access_code_cast_handler.h
index 4b80259..a07fd5e 100644
--- a/chrome/browser/ui/webui/access_code_cast/access_code_cast_handler.h
+++ b/chrome/browser/ui/webui/access_code_cast/access_code_cast_handler.h
@@ -150,8 +150,6 @@
 
   // The id of the media sink discovered from the access code;
   absl::optional<MediaSink::Id> sink_id_;
-  // Set of cast modes supported by the discovered sink;
-  media_router::CastModeSet supported_cast_modes_;
 
   // Monitors and reports sink availability.
   std::unique_ptr<QueryResultManager> query_result_manager_;
diff --git a/chrome/browser/ui/webui/access_code_cast/access_code_cast_ui.cc b/chrome/browser/ui/webui/access_code_cast/access_code_cast_ui.cc
index 9a88da8..43adbbf8 100644
--- a/chrome/browser/ui/webui/access_code_cast/access_code_cast_ui.cc
+++ b/chrome/browser/ui/webui/access_code_cast/access_code_cast_ui.cc
@@ -34,13 +34,11 @@
 
 using media_router::AccessCodeCastHandler;
 
-// Creates default params for showing AccessCodeCastDialog in ChromeOS
+// Creates default params for showing AccessCodeCastDialog
 views::Widget::InitParams CreateParams() {
   views::Widget::InitParams params;
   params.remove_standard_frame = true;
   params.corner_radius = 12;
-  // Dialog frame view has its own shadow.
-  params.shadow_type = views::Widget::InitParams::ShadowType::kNone;
   params.type = views::Widget::InitParams::Type::TYPE_BUBBLE;
   // Make sure the dialog border is rendered correctly
   params.opacity = views::Widget::InitParams::WindowOpacity::kTranslucent;
diff --git a/chrome/browser/ui/webui/chromeos/login/l10n_util.cc b/chrome/browser/ui/webui/chromeos/login/l10n_util.cc
index cdede97..5243d78 100644
--- a/chrome/browser/ui/webui/chromeos/login/l10n_util.cc
+++ b/chrome/browser/ui/webui/chromeos/login/l10n_util.cc
@@ -52,18 +52,16 @@
 constexpr char16_t kMostRelevantLanguagesDivider16[] =
     u"MOST_RELEVANT_LANGUAGES_DIVIDER";
 
-std::unique_ptr<base::DictionaryValue> CreateInputMethodsEntry(
+base::Value CreateInputMethodsEntry(
     const input_method::InputMethodDescriptor& method,
     const std::string selected,
     input_method::InputMethodUtil* util) {
   const std::string& ime_id = method.id();
-  std::unique_ptr<base::DictionaryValue> input_method(
-      new base::DictionaryValue);
-  input_method->GetDict().Set("value", ime_id);
-  input_method->GetDict().Set("title",
-                              util->GetInputMethodLongNameStripped(method));
-  input_method->GetDict().Set("selected", ime_id == selected);
-  return input_method;
+  base::Value::Dict input_method;
+  input_method.Set("value", ime_id);
+  input_method.Set("title", util->GetInputMethodLongNameStripped(method));
+  input_method.Set("selected", ime_id == selected);
+  return base::Value(std::move(input_method));
 }
 
 // Returns true if element was inserted.
@@ -74,14 +72,13 @@
 }
 
 void AddOptgroupOtherLayouts(base::ListValue* input_methods_list) {
-  std::unique_ptr<base::DictionaryValue> optgroup(new base::DictionaryValue);
-  optgroup->GetDict().Set(
-      "optionGroupName",
-      l10n_util::GetStringUTF16(IDS_OOBE_OTHER_KEYBOARD_LAYOUTS));
-  input_methods_list->Append(std::move(optgroup));
+  base::Value::Dict optgroup;
+  optgroup.Set("optionGroupName",
+               l10n_util::GetStringUTF16(IDS_OOBE_OTHER_KEYBOARD_LAYOUTS));
+  input_methods_list->Append(base::Value(std::move(optgroup)));
 }
 
-std::unique_ptr<base::DictionaryValue> CreateLanguageEntry(
+base::Value CreateLanguageEntry(
     const std::string& language_code,
     const std::u16string& language_display_name,
     const std::u16string& language_native_display_name) {
@@ -92,14 +89,14 @@
 
   const bool has_rtl_chars =
       base::i18n::StringContainsStrongRTLChars(display_name);
-  const std::string directionality = has_rtl_chars ? "rtl" : "ltr";
+  const char* directionality = has_rtl_chars ? "rtl" : "ltr";
 
-  auto dictionary = std::make_unique<base::DictionaryValue>();
-  dictionary->GetDict().Set("code", language_code);
-  dictionary->GetDict().Set("displayName", language_display_name);
-  dictionary->GetDict().Set("textDirection", directionality);
-  dictionary->GetDict().Set("nativeDisplayName", language_native_display_name);
-  return dictionary;
+  base::Value::Dict dictionary;
+  dictionary.Set("code", language_code);
+  dictionary.Set("displayName", language_display_name);
+  dictionary.Set("textDirection", directionality);
+  dictionary.Set("nativeDisplayName", language_native_display_name);
+  return base::Value(std::move(dictionary));
 }
 
 // Gets the list of languages with `descriptors` based on `base_language_codes`.
@@ -270,9 +267,9 @@
     std::u16string display_name(out_display_names[i]);
     if (insert_divider && display_name == divider16) {
       // Insert divider.
-      auto dictionary = std::make_unique<base::DictionaryValue>();
-      dictionary->GetDict().Set("code", kMostRelevantLanguagesDivider);
-      language_list->Append(std::move(dictionary));
+      base::Value::Dict dictionary;
+      dictionary.Set("code", kMostRelevantLanguagesDivider);
+      language_list->Append(base::Value(std::move(dictionary)));
       continue;
     }
 
diff --git a/chrome/browser/ui/webui/settings/chromeos/multidevice_handler.cc b/chrome/browser/ui/webui/settings/chromeos/multidevice_handler.cc
index b31dbdb9..e14dac2 100644
--- a/chrome/browser/ui/webui/settings/chromeos/multidevice_handler.cc
+++ b/chrome/browser/ui/webui/settings/chromeos/multidevice_handler.cc
@@ -66,6 +66,8 @@
     "isPhoneHubPermissionsDialogSupported";
 const char kIsCameraRollFilePermissionGranted[] =
     "isCameraRollFilePermissionGranted";
+const char kIsPhoneHubFeatureCombinedSetupSupported[] =
+    "isPhoneHubFeatureCombinedSetupSupported";
 
 constexpr char kAndroidSmsInfoOriginKey[] = "origin";
 constexpr char kAndroidSmsInfoEnabledKey[] = "enabled";
@@ -161,6 +163,15 @@
       "cancelAppsSetup",
       base::BindRepeating(&MultideviceHandler::HandleCancelAppsSetup,
                           base::Unretained(this)));
+  web_ui()->RegisterMessageCallback(
+      "attemptCombinedFeatureSetup",
+      base::BindRepeating(
+          &MultideviceHandler::HandleAttemptCombinedFeatureSetup,
+          base::Unretained(this)));
+  web_ui()->RegisterMessageCallback(
+      "cancelCombinedFeatureSetup",
+      base::BindRepeating(&MultideviceHandler::HandleCancelCombinedFeatureSetup,
+                          base::Unretained(this)));
 }
 
 void MultideviceHandler::OnJavascriptAllowed() {
@@ -274,6 +285,10 @@
   UpdatePageContent();
 }
 
+void MultideviceHandler::OnFeatureSetupRequestSupportedChanged() {
+  UpdatePageContent();
+}
+
 void MultideviceHandler::OnPairingStateChanged() {
   UpdatePageContent();
   NotifyAndroidSmsInfoChange();
@@ -516,7 +531,60 @@
   apps_access_operation_.reset();
 }
 
-void MultideviceHandler::OnStatusChange(
+void MultideviceHandler::HandleAttemptCombinedFeatureSetup(
+    const base::Value::List& args) {
+  bool camera_roll = false;
+  if (args[0].is_bool())
+    camera_roll = args[0].GetBool();
+  bool notifications = false;
+  if (args[1].is_bool())
+    notifications = args[1].GetBool();
+
+  DCHECK(features::IsPhoneHubEnabled());
+  DCHECK(!combined_access_operation_);
+
+  if (!multidevice_feature_access_manager_->GetFeatureSetupRequestSupported()) {
+    PA_LOG(WARNING) << "Cannot request combined access setup flow; "
+                    << "FeatureSetupRequest is not supported by the phone.";
+    return;
+  }
+
+  phonehub::MultideviceFeatureAccessManager::AccessStatus
+      notification_access_status =
+          multidevice_feature_access_manager_->GetNotificationAccessStatus();
+  phonehub::MultideviceFeatureAccessManager::AccessStatus
+      camera_roll_access_status =
+          multidevice_feature_access_manager_->GetCameraRollAccessStatus();
+  if (camera_roll_access_status != phonehub::MultideviceFeatureAccessManager::
+                                       AccessStatus::kAvailableButNotGranted &&
+      camera_roll) {
+    PA_LOG(WARNING) << "Cannot request combined access setup flow; current "
+                    << "Camera Roll status: " << camera_roll_access_status;
+    return;
+  }
+  if (notification_access_status != phonehub::MultideviceFeatureAccessManager::
+                                        AccessStatus::kAvailableButNotGranted &&
+      notifications) {
+    PA_LOG(WARNING) << "Cannot request combined access setup flow; current "
+                    << "Notification status: " << notification_access_status;
+    return;
+  }
+
+  combined_access_operation_ =
+      multidevice_feature_access_manager_->AttemptCombinedFeatureSetup(
+          camera_roll, notifications, /*delegate=*/this);
+  DCHECK(combined_access_operation_);
+}
+
+void MultideviceHandler::HandleCancelCombinedFeatureSetup(
+    const base::Value::List& args) {
+  DCHECK(features::IsPhoneHubEnabled());
+  DCHECK(combined_access_operation_);
+
+  combined_access_operation_.reset();
+}
+
+void MultideviceHandler::OnNotificationStatusChange(
     phonehub::NotificationAccessSetupOperation::Status new_status) {
   FireWebUIListener("settings.onNotificationAccessSetupStatusChanged",
                     base::Value(static_cast<int32_t>(new_status)));
@@ -534,6 +602,15 @@
     apps_access_operation_.reset();
 }
 
+void MultideviceHandler::OnCombinedStatusChange(
+    phonehub::CombinedAccessSetupOperation::Status new_status) {
+  FireWebUIListener("settings.onCombinedAccessSetupStatusChanged",
+                    base::Value(static_cast<int32_t>(new_status)));
+
+  if (phonehub::CombinedAccessSetupOperation::IsFinalStatus(new_status))
+    combined_access_operation_.reset();
+}
+
 void MultideviceHandler::OnSetFeatureStateEnabledResult(
     const std::string& js_callback_id,
     bool success) {
@@ -677,6 +754,13 @@
       kIsPhoneHubPermissionsDialogSupported,
       is_phone_hub_permissions_dialog_supported);
 
+  page_content_dictionary->SetBoolKey(
+      kIsPhoneHubFeatureCombinedSetupSupported,
+      multidevice_feature_access_manager_
+          ? multidevice_feature_access_manager_
+                ->GetFeatureSetupRequestSupported()
+          : false);
+
   return page_content_dictionary;
 }
 
diff --git a/chrome/browser/ui/webui/settings/chromeos/multidevice_handler.h b/chrome/browser/ui/webui/settings/chromeos/multidevice_handler.h
index babc030c..8a312d6 100644
--- a/chrome/browser/ui/webui/settings/chromeos/multidevice_handler.h
+++ b/chrome/browser/ui/webui/settings/chromeos/multidevice_handler.h
@@ -7,6 +7,7 @@
 
 #include "ash/components/multidevice/remote_device_ref.h"
 #include "ash/components/phonehub/camera_roll_manager.h"
+#include "ash/components/phonehub/combined_access_setup_operation.h"
 #include "ash/components/phonehub/multidevice_feature_access_manager.h"
 #include "ash/components/phonehub/notification_access_setup_operation.h"
 #include "ash/services/multidevice_setup/public/cpp/multidevice_setup_client.h"
@@ -38,7 +39,8 @@
       public phonehub::NotificationAccessSetupOperation::Delegate,
       public ash::eche_app::AppsAccessManager::Observer,
       public ash::eche_app::AppsAccessSetupOperation::Delegate,
-      public ash::phonehub::CameraRollManager::Observer {
+      public ash::phonehub::CameraRollManager::Observer,
+      public phonehub::CombinedAccessSetupOperation::Delegate {
  public:
   MultideviceHandler(
       PrefService* prefs,
@@ -74,16 +76,21 @@
           feature_states_map) override;
 
   // NotificationAccessSetupOperation::Delegate:
-  void OnStatusChange(
+  void OnNotificationStatusChange(
       phonehub::NotificationAccessSetupOperation::Status new_status) override;
 
   // ash::eche_app::AppsAccessSetupOperation::Delegate:
   void OnAppsStatusChange(
       ash::eche_app::AppsAccessSetupOperation::Status new_status) override;
 
+  // CombinedAccessSetupOperation::Delegate:
+  void OnCombinedStatusChange(
+      phonehub::CombinedAccessSetupOperation::Status new_status) override;
+
   // phonehub::MultideviceFeatureAccessManager::Observer:
   void OnNotificationAccessChanged() override;
   void OnCameraRollAccessChanged() override;
+  void OnFeatureSetupRequestSupportedChanged() override;
 
   // multidevice_setup::AndroidSmsPairingStateTracker::Observer:
   void OnPairingStateChanged() override;
@@ -118,6 +125,8 @@
   void HandleCancelNotificationSetup(const base::Value::List& args);
   void HandleAttemptAppsSetup(const base::Value::List& args);
   void HandleCancelAppsSetup(const base::Value::List& args);
+  void HandleAttemptCombinedFeatureSetup(const base::Value::List& args);
+  void HandleCancelCombinedFeatureSetup(const base::Value::List& args);
 
   void OnSetFeatureStateEnabledResult(const std::string& js_callback_id,
                                       bool success);
@@ -155,6 +164,8 @@
       multidevice_feature_access_manager_;
   std::unique_ptr<phonehub::NotificationAccessSetupOperation>
       notification_access_operation_;
+  std::unique_ptr<phonehub::CombinedAccessSetupOperation>
+      combined_access_operation_;
 
   multidevice_setup::AndroidSmsPairingStateTracker*
       android_sms_pairing_state_tracker_;
diff --git a/chrome/browser/ui/webui/settings/chromeos/multidevice_handler_unittest.cc b/chrome/browser/ui/webui/settings/chromeos/multidevice_handler_unittest.cc
index f444464..ee262e9d 100644
--- a/chrome/browser/ui/webui/settings/chromeos/multidevice_handler_unittest.cc
+++ b/chrome/browser/ui/webui/settings/chromeos/multidevice_handler_unittest.cc
@@ -102,7 +102,8 @@
     bool expected_is_nearby_share_disallowed_by_policy_,
     bool expected_is_phone_hub_apps_access_granted_,
     bool expected_is_camera_roll_file_permission_granted_,
-    bool expected_is_camera_roll_access_status_granted_) {
+    bool expected_is_camera_roll_access_status_granted_,
+    bool expected_is_feature_setup_request_supported_) {
   const base::DictionaryValue* page_content_dict;
   EXPECT_TRUE(value->GetAsDictionary(&page_content_dict));
 
@@ -200,6 +201,10 @@
 
   EXPECT_THAT(page_content_dict->FindIntKey("cameraRollAccessStatus"),
               Optional(expected_is_camera_roll_access_status_granted_ ? 2 : 1));
+
+  EXPECT_THAT(
+      page_content_dict->FindBoolKey("isPhoneHubFeatureCombinedSetupSupported"),
+      Optional(expected_is_feature_setup_request_supported_));
 }
 
 }  // namespace
@@ -265,6 +270,21 @@
         {});
   }
 
+  void SetUpHandlerWithEmptyManagers() {
+    handler_.reset();
+    test_web_ui_.reset();
+    handler_ = std::make_unique<TestMultideviceHandler>(
+        prefs_.get(), fake_multidevice_setup_client_.get(), nullptr, nullptr,
+        nullptr, nullptr, nullptr);
+
+    test_web_ui_ = std::make_unique<content::TestWebUI>();
+    test_web_ui_->set_web_contents(test_web_contents_.get());
+    handler_->set_web_ui(test_web_ui_.get());
+
+    handler_->RegisterMessages();
+    handler_->AllowJavascript();
+  }
+
   void CallGetPageContentData() {
     size_t call_data_count_before_call = test_web_ui()->call_data().size();
 
@@ -351,6 +371,26 @@
     test_web_ui()->HandleReceivedMessage("cancelAppsSetup", &empty_args);
   }
 
+  void CallAttemptCameraRollSetup(bool has_camera_roll_access_been_granted) {
+    fake_multidevice_feature_access_manager()
+        ->SetCameraRollAccessStatusInternal(
+            has_camera_roll_access_been_granted
+                ? phonehub::MultideviceFeatureAccessManager::AccessStatus::
+                      kAccessGranted
+                : phonehub::MultideviceFeatureAccessManager::AccessStatus::
+                      kAvailableButNotGranted);
+    base::ListValue args;
+    args.Append(/*camera_roll=*/true);
+    args.Append(/*notifications=*/false);
+    test_web_ui()->HandleReceivedMessage("attemptCombinedFeatureSetup", &args);
+  }
+
+  void CallCancelCameraRollSetup() {
+    base::ListValue empty_args;
+    test_web_ui()->HandleReceivedMessage("cancelCombinedFeatureSetup",
+                                         &empty_args);
+  }
+
   void SimulateHostStatusUpdate(
       multidevice_setup::mojom::HostStatus host_status,
       const absl::optional<multidevice::RemoteDeviceRef>& host_device) {
@@ -608,7 +648,7 @@
 
   bool IsNotificationAccessSetupOperationInProgress() {
     return fake_multidevice_feature_access_manager()
-        ->IsSetupOperationInProgress();
+        ->IsNotificationSetupOperationInProgress();
   }
 
   void SimulateAppsOptInStatusChange(
@@ -637,12 +677,41 @@
     return fake_apps_access_manager()->IsSetupOperationInProgress();
   }
 
+  void SimulateCameraRollOptInStatusChange(
+      phonehub::CombinedAccessSetupOperation::Status status) {
+    size_t call_data_count_before_call = test_web_ui()->call_data().size();
+
+    fake_multidevice_feature_access_manager()->SetCombinedSetupOperationStatus(
+        status);
+
+    bool completed_successfully =
+        status ==
+        phonehub::CombinedAccessSetupOperation::Status::kCompletedSuccessfully;
+    if (completed_successfully)
+      call_data_count_before_call++;
+
+    EXPECT_EQ(call_data_count_before_call + 1u,
+              test_web_ui()->call_data().size());
+    const content::TestWebUI::CallData& call_data =
+        CallDataAtIndex(call_data_count_before_call);
+    EXPECT_EQ("cr.webUIListenerCallback", call_data.function_name());
+    EXPECT_EQ("settings.onCombinedAccessSetupStatusChanged",
+              call_data.arg1()->GetString());
+    EXPECT_EQ(call_data.arg2()->GetInt(), static_cast<int32_t>(status));
+  }
+
+  bool IsCameraRollAccessSetupOperationInProgress() {
+    return fake_multidevice_feature_access_manager()
+        ->IsCombinedSetupOperationInProgress();
+  }
+
   const multidevice::RemoteDeviceRef test_device_;
 
   bool expected_is_nearby_share_disallowed_by_policy_ = false;
   bool expected_is_phone_hub_apps_access_granted_ = false;
   bool expected_is_camera_roll_file_permission_granted_ = true;
   bool expected_is_camera_roll_access_status_granted_ = false;
+  bool expected_is_feature_setup_request_supported_ = false;
 
  private:
   void VerifyPageContent(const base::Value* value) {
@@ -653,7 +722,8 @@
         expected_is_nearby_share_disallowed_by_policy_,
         expected_is_phone_hub_apps_access_granted_,
         expected_is_camera_roll_file_permission_granted_,
-        expected_is_camera_roll_access_status_granted_);
+        expected_is_camera_roll_access_status_granted_,
+        expected_is_feature_setup_request_supported_);
   }
 
   content::BrowserTaskEnvironment task_environment_;
@@ -684,6 +754,15 @@
   base::test::ScopedFeatureList scoped_feature_list_;
 };
 
+TEST_F(MultideviceHandlerTest, PageContentDataRequestedWithNullManagers) {
+  SetUpHandlerWithEmptyManagers();
+
+  base::Value args(base::Value::Type::LIST);
+  args.Append("handlerFunctionName");
+  test_web_ui()->HandleReceivedMessage("getPageContentData",
+                                       &base::Value::AsListValue(args));
+}
+
 TEST_F(MultideviceHandlerTest, NotificationSetupFlow) {
   using Status = phonehub::NotificationAccessSetupOperation::Status;
 
@@ -782,6 +861,57 @@
   EXPECT_FALSE(IsAppsAccessSetupOperationInProgress());
 }
 
+TEST_F(MultideviceHandlerTest, CameraRollSetupFlow) {
+  using Status = phonehub::CombinedAccessSetupOperation::Status;
+  fake_multidevice_feature_access_manager()
+      ->SetFeatureSetupRequestSupportedInternal(true);
+
+  // Simulate success flow.
+  CallAttemptCameraRollSetup(/*has_access_been_granted=*/false);
+  EXPECT_TRUE(IsCameraRollAccessSetupOperationInProgress());
+
+  SimulateCameraRollOptInStatusChange(Status::kConnecting);
+  EXPECT_TRUE(IsCameraRollAccessSetupOperationInProgress());
+
+  SimulateCameraRollOptInStatusChange(
+      Status::kSentMessageToPhoneAndWaitingForResponse);
+  EXPECT_TRUE(IsCameraRollAccessSetupOperationInProgress());
+
+  SimulateCameraRollOptInStatusChange(Status::kCompletedSuccessfully);
+  EXPECT_FALSE(IsCameraRollAccessSetupOperationInProgress());
+
+  // Simulate cancel flow.
+  CallAttemptCameraRollSetup(/*has_access_been_granted=*/false);
+  EXPECT_TRUE(IsCameraRollAccessSetupOperationInProgress());
+
+  CallCancelCameraRollSetup();
+  EXPECT_FALSE(IsCameraRollAccessSetupOperationInProgress());
+
+  // Simulate failure via time-out flow.
+  CallAttemptCameraRollSetup(/*has_access_been_granted=*/false);
+  EXPECT_TRUE(IsCameraRollAccessSetupOperationInProgress());
+
+  SimulateCameraRollOptInStatusChange(Status::kConnecting);
+  EXPECT_TRUE(IsCameraRollAccessSetupOperationInProgress());
+
+  SimulateCameraRollOptInStatusChange(Status::kTimedOutConnecting);
+  EXPECT_FALSE(IsCameraRollAccessSetupOperationInProgress());
+
+  // Simulate failure via connected then disconnected flow.
+  CallAttemptCameraRollSetup(/*has_access_been_granted=*/false);
+  EXPECT_TRUE(IsCameraRollAccessSetupOperationInProgress());
+
+  SimulateCameraRollOptInStatusChange(Status::kConnecting);
+  EXPECT_TRUE(IsCameraRollAccessSetupOperationInProgress());
+
+  SimulateCameraRollOptInStatusChange(Status::kConnectionDisconnected);
+  EXPECT_FALSE(IsCameraRollAccessSetupOperationInProgress());
+
+  // If access has already been granted, a setup operation should not occur.
+  CallAttemptCameraRollSetup(/*has_access_been_granted=*/true);
+  EXPECT_FALSE(IsCameraRollAccessSetupOperationInProgress());
+}
+
 TEST_F(MultideviceHandlerTest, PageContentData) {
   CallGetPageContentData();
   CallGetPageContentData();
diff --git a/chrome/browser/ui/webui/signin/enterprise_profile_welcome_handler.cc b/chrome/browser/ui/webui/signin/enterprise_profile_welcome_handler.cc
index 512302e..52fd797 100644
--- a/chrome/browser/ui/webui/signin/enterprise_profile_welcome_handler.cc
+++ b/chrome/browser/ui/webui/signin/enterprise_profile_welcome_handler.cc
@@ -94,7 +94,7 @@
     EnterpriseProfileWelcomeUI::ScreenType type,
     const AccountInfo& account_info,
     absl::optional<SkColor> profile_color,
-    base::OnceCallback<void(bool)> proceed_callback)
+    signin::SigninChoiceCallback proceed_callback)
     : browser_(browser),
       type_(type),
       email_(base::UTF8ToUTF16(account_info.email)),
@@ -112,12 +112,12 @@
 
 EnterpriseProfileWelcomeHandler::~EnterpriseProfileWelcomeHandler() {
   BrowserList::RemoveObserver(this);
-  HandleCancel(nullptr);
+  HandleCancel(base::Value::List());
 }
 
 void EnterpriseProfileWelcomeHandler::RegisterMessages() {
   profile_path_ = Profile::FromWebUI(web_ui())->GetPath();
-  web_ui()->RegisterDeprecatedMessageCallback(
+  web_ui()->RegisterMessageCallback(
       "initialized",
       base::BindRepeating(&EnterpriseProfileWelcomeHandler::HandleInitialized,
                           base::Unretained(this)));
@@ -126,11 +126,11 @@
       base::BindRepeating(
           &EnterpriseProfileWelcomeHandler::HandleInitializedWithSize,
           base::Unretained(this)));
-  web_ui()->RegisterDeprecatedMessageCallback(
+  web_ui()->RegisterMessageCallback(
       "proceed",
       base::BindRepeating(&EnterpriseProfileWelcomeHandler::HandleProceed,
                           base::Unretained(this)));
-  web_ui()->RegisterDeprecatedMessageCallback(
+  web_ui()->RegisterMessageCallback(
       "cancel",
       base::BindRepeating(&EnterpriseProfileWelcomeHandler::HandleCancel,
                           base::Unretained(this)));
@@ -181,10 +181,10 @@
 }
 
 void EnterpriseProfileWelcomeHandler::HandleInitialized(
-    const base::ListValue* args) {
-  CHECK_EQ(1u, args->GetListDeprecated().size());
+    const base::Value::List& args) {
+  CHECK_EQ(1u, args.size());
   AllowJavascript();
-  const base::Value& callback_id = args->GetListDeprecated()[0];
+  const base::Value& callback_id = args[0];
   ResolveJavascriptCallback(callback_id, GetProfileInfoValue());
 }
 
@@ -197,15 +197,20 @@
 }
 
 void EnterpriseProfileWelcomeHandler::HandleProceed(
-    const base::ListValue* args) {
-  if (proceed_callback_)
-    std::move(proceed_callback_).Run(true);
+    const base::Value::List& args) {
+  CHECK_EQ(1u, args.size());
+  if (proceed_callback_) {
+    bool use_existing_profile = args[0].GetIfBool().value_or(false);
+    std::move(proceed_callback_)
+        .Run(use_existing_profile ? signin::SIGNIN_CHOICE_CONTINUE
+                                  : signin::SIGNIN_CHOICE_NEW_PROFILE);
+  }
 }
 
 void EnterpriseProfileWelcomeHandler::HandleCancel(
-    const base::ListValue* args) {
+    const base::Value::List& args) {
   if (proceed_callback_)
-    std::move(proceed_callback_).Run(false);
+    std::move(proceed_callback_).Run(signin::SIGNIN_CHOICE_CANCEL);
 }
 
 void EnterpriseProfileWelcomeHandler::UpdateProfileInfo(
@@ -306,7 +311,7 @@
 }
 
 void EnterpriseProfileWelcomeHandler::CallProceedCallbackForTesting(
-    bool proceed) {
+    signin::SigninChoice choice) {
   if (proceed_callback_)
-    std::move(proceed_callback_).Run(proceed);
+    std::move(proceed_callback_).Run(choice);
 }
diff --git a/chrome/browser/ui/webui/signin/enterprise_profile_welcome_handler.h b/chrome/browser/ui/webui/signin/enterprise_profile_welcome_handler.h
index f4bab869..49c186de 100644
--- a/chrome/browser/ui/webui/signin/enterprise_profile_welcome_handler.h
+++ b/chrome/browser/ui/webui/signin/enterprise_profile_welcome_handler.h
@@ -15,6 +15,7 @@
 #include "chrome/browser/profiles/profile_attributes_storage.h"
 #include "chrome/browser/ui/browser_list_observer.h"
 #include "chrome/browser/ui/webui/signin/enterprise_profile_welcome_ui.h"
+#include "chrome/browser/ui/webui/signin/signin_utils.h"
 #include "components/signin/public/identity_manager/identity_manager.h"
 #include "content/public/browser/web_ui_message_handler.h"
 #include "google_apis/gaia/core_account_id.h"
@@ -41,7 +42,7 @@
       EnterpriseProfileWelcomeUI::ScreenType type,
       const AccountInfo& account_info,
       absl::optional<SkColor> profile_color,
-      base::OnceCallback<void(bool)> proceed_callback);
+      signin::SigninChoiceCallback proceed_callback);
   ~EnterpriseProfileWelcomeHandler() override;
 
   EnterpriseProfileWelcomeHandler(const EnterpriseProfileWelcomeHandler&) =
@@ -69,16 +70,16 @@
 
   // Access to construction parameters for tests.
   EnterpriseProfileWelcomeUI::ScreenType GetTypeForTesting();
-  void CallProceedCallbackForTesting(bool proceed);
+  void CallProceedCallbackForTesting(signin::SigninChoice choice);
 
  private:
-  void HandleInitialized(const base::ListValue* args);
+  void HandleInitialized(const base::Value::List& args);
   // Handles the web ui message sent when the html content is done being laid
   // out and it's time to resize the native view hosting it to fit. |args| is
   // a single integer value for the height the native view should resize to.
   void HandleInitializedWithSize(const base::ListValue* args);
-  void HandleProceed(const base::ListValue* args);
-  void HandleCancel(const base::ListValue* args);
+  void HandleProceed(const base::Value::List& args);
+  void HandleCancel(const base::Value::List& args);
 
   // Sends an updated profile info (avatar and colors) to the WebUI.
   // `profile_path` is the path of the profile being updated, this function does
@@ -108,7 +109,7 @@
   const std::string domain_name_;
   const CoreAccountId account_id_;
   absl::optional<SkColor> profile_color_;
-  base::OnceCallback<void(bool)> proceed_callback_;
+  signin::SigninChoiceCallback proceed_callback_;
 };
 
 #endif  // CHROME_BROWSER_UI_WEBUI_SIGNIN_ENTERPRISE_PROFILE_WELCOME_HANDLER_H_
diff --git a/chrome/browser/ui/webui/signin/enterprise_profile_welcome_ui.cc b/chrome/browser/ui/webui/signin/enterprise_profile_welcome_ui.cc
index a041fbf5..a4f7360 100644
--- a/chrome/browser/ui/webui/signin/enterprise_profile_welcome_ui.cc
+++ b/chrome/browser/ui/webui/signin/enterprise_profile_welcome_ui.cc
@@ -7,6 +7,7 @@
 #include "base/callback_helpers.h"
 #include "chrome/browser/profiles/profile.h"
 #include "chrome/browser/ui/webui/signin/enterprise_profile_welcome_handler.h"
+#include "chrome/browser/ui/webui/signin/signin_utils.h"
 #include "chrome/browser/ui/webui/webui_util.h"
 #include "chrome/common/webui_url_constants.h"
 #include "chrome/grit/chromium_strings.h"
@@ -43,6 +44,11 @@
   source->AddLocalizedString("enterpriseProfileWelcomeTitle",
                              IDS_ENTERPRISE_PROFILE_WELCOME_TITLE);
   source->AddLocalizedString("cancelLabel", IDS_CANCEL);
+  source->AddLocalizedString("proceedAlternateLabel",
+                             IDS_WELCOME_SIGNIN_VIEW_SIGNIN);
+  source->AddLocalizedString("linkDataText",
+                             IDS_ENTERPRISE_PROFILE_WELCOME_LINK_DATA_CHECKBOX);
+  source->AddBoolean("showLinkDataCheckbox", false);
   source->AddBoolean("isModalDialog", false);
 
   content::WebUIDataSource::Add(Profile::FromWebUI(web_ui), source);
@@ -54,8 +60,9 @@
     Browser* browser,
     EnterpriseProfileWelcomeUI::ScreenType type,
     const AccountInfo& account_info,
+    bool force_new_profile,
     absl::optional<SkColor> profile_color,
-    base::OnceCallback<void(bool)> proceed_callback) {
+    signin::SigninChoiceCallback proceed_callback) {
   auto handler = std::make_unique<EnterpriseProfileWelcomeHandler>(
       browser, type, account_info, profile_color, std::move(proceed_callback));
   handler_ = handler.get();
@@ -64,10 +71,15 @@
       EnterpriseProfileWelcomeUI::ScreenType::kEnterpriseAccountCreation) {
     base::DictionaryValue update_data;
     update_data.SetBoolKey("isModalDialog", true);
-    update_data.SetStringKey(
-        "enterpriseProfileWelcomeTitle",
-        l10n_util::GetStringUTF16(
-            IDS_ENTERPRISE_WELCOME_PROFILE_REQUIRED_TITLE));
+
+    int title_id = force_new_profile
+                       ? IDS_ENTERPRISE_WELCOME_PROFILE_REQUIRED_TITLE
+                       : IDS_ENTERPRISE_WELCOME_PROFILE_WILL_BE_MANAGED_TITLE;
+    update_data.SetStringKey("enterpriseProfileWelcomeTitle",
+                             l10n_util::GetStringUTF16(title_id));
+    if (force_new_profile)
+      update_data.SetBoolKey("showLinkDataCheckbox", true);
+
     content::WebUIDataSource::Update(
         Profile::FromWebUI(web_ui()),
         chrome::kChromeUIEnterpriseProfileWelcomeHost,
diff --git a/chrome/browser/ui/webui/signin/enterprise_profile_welcome_ui.h b/chrome/browser/ui/webui/signin/enterprise_profile_welcome_ui.h
index 3fa43da8..e7e62e6 100644
--- a/chrome/browser/ui/webui/signin/enterprise_profile_welcome_ui.h
+++ b/chrome/browser/ui/webui/signin/enterprise_profile_welcome_ui.h
@@ -7,6 +7,7 @@
 
 #include "base/callback.h"
 #include "base/memory/raw_ptr.h"
+#include "chrome/browser/ui/webui/signin/signin_utils.h"
 #include "content/public/browser/web_ui_controller.h"
 #include "third_party/abseil-cpp/absl/types/optional.h"
 #include "third_party/skia/include/core/SkColor.h"
@@ -40,8 +41,9 @@
   void Initialize(Browser* browser,
                   ScreenType type,
                   const AccountInfo& account_info,
+                  bool force_new_profile,
                   absl::optional<SkColor> profile_color,
-                  base::OnceCallback<void(bool)> proceed_callback);
+                  signin::SigninChoiceCallback proceed_callback);
 
   // Allows tests to trigger page events.
   EnterpriseProfileWelcomeHandler* GetHandlerForTesting();
diff --git a/chrome/browser/ui/webui/signin/turn_sync_on_helper_delegate_impl.cc b/chrome/browser/ui/webui/signin/turn_sync_on_helper_delegate_impl.cc
index 16cdfdc..30e00c1a 100644
--- a/chrome/browser/ui/webui/signin/turn_sync_on_helper_delegate_impl.cc
+++ b/chrome/browser/ui/webui/signin/turn_sync_on_helper_delegate_impl.cc
@@ -82,16 +82,19 @@
           ->GetProfileAttributesStorage()
           .GetProfileAttributesWithPath(browser->profile()->GetPath());
   browser->signin_view_controller()->ShowModalEnterpriseConfirmationDialog(
-      account_info, GenerateNewProfileColor(entry).color,
+      account_info, /*force_new_profile=*/true,
+      GenerateNewProfileColor(entry).color,
       base::BindOnce(
           [](signin::SigninChoiceCallback callback, Browser* browser,
-             bool prompt_for_new_profile, bool create_profile) {
+             bool prompt_for_new_profile, signin::SigninChoice choice) {
             browser->signin_view_controller()->CloseModalSignin();
-            std::move(callback).Run(
-                create_profile ? prompt_for_new_profile
-                                     ? signin::SIGNIN_CHOICE_NEW_PROFILE
-                                     : signin::SIGNIN_CHOICE_CONTINUE
-                               : signin::SIGNIN_CHOICE_CANCEL);
+            signin::SigninChoice result = signin::SIGNIN_CHOICE_CANCEL;
+            if (choice != signin::SIGNIN_CHOICE_CANCEL) {
+              result = prompt_for_new_profile
+                           ? signin::SIGNIN_CHOICE_NEW_PROFILE
+                           : signin::SIGNIN_CHOICE_CONTINUE;
+            }
+            std::move(callback).Run(result);
           },
           std::move(callback), browser.get(), prompt_for_new_profile));
 }
diff --git a/chrome/browser/web_applications/BUILD.gn b/chrome/browser/web_applications/BUILD.gn
index a4019745..34663f0 100644
--- a/chrome/browser/web_applications/BUILD.gn
+++ b/chrome/browser/web_applications/BUILD.gn
@@ -379,6 +379,8 @@
     "system_web_apps/test/test_system_web_app_url_data_source.h",
     "system_web_apps/test/test_system_web_app_web_ui_controller_factory.cc",
     "system_web_apps/test/test_system_web_app_web_ui_controller_factory.h",
+    "test/app_registration_waiter.cc",
+    "test/app_registration_waiter.h",
     "test/fake_data_retriever.cc",
     "test/fake_data_retriever.h",
     "test/fake_externally_managed_app_manager.cc",
@@ -442,6 +444,7 @@
     "//chrome/browser/ui",
     "//components/crx_file:crx_file",
     "//components/keyed_service/content",
+    "//components/services/app_service/public/cpp:app_update",
     "//components/services/app_service/public/cpp:app_url_handling",
     "//components/services/app_service/public/cpp:types",
     "//components/sync:test_support_model",
diff --git a/chrome/browser/web_applications/app_service/lacros_web_apps_controller_lacros_browsertest.cc b/chrome/browser/web_applications/app_service/lacros_web_apps_controller_lacros_browsertest.cc
index f0d59e9d..0a7f8e9e 100644
--- a/chrome/browser/web_applications/app_service/lacros_web_apps_controller_lacros_browsertest.cc
+++ b/chrome/browser/web_applications/app_service/lacros_web_apps_controller_lacros_browsertest.cc
@@ -10,6 +10,7 @@
 #include "chrome/browser/ui/browser_window.h"
 #include "chrome/browser/ui/web_applications/web_app_controller_browsertest.h"
 #include "chrome/browser/web_applications/app_service/lacros_web_apps_controller.h"
+#include "chrome/browser/web_applications/test/app_registration_waiter.h"
 #include "chrome/browser/web_applications/web_app_utils.h"
 #include "chromeos/crosapi/mojom/app_service_types.mojom.h"
 #include "chromeos/crosapi/mojom/test_controller.mojom-test-utils.h"
@@ -21,40 +22,37 @@
 
 namespace web_app {
 
-using LacrosWebAppsControllerBrowserTest = web_app::WebAppControllerBrowserTest;
+class LacrosWebAppsControllerBrowserTest : public WebAppControllerBrowserTest {
+ public:
+  LacrosWebAppsControllerBrowserTest() = default;
+  ~LacrosWebAppsControllerBrowserTest() override = default;
 
-// TODO(crbug.com/1309148): Disabled for flakiness.
-// Test that the default context menu for a web app has the correct items.
-IN_PROC_BROWSER_TEST_F(LacrosWebAppsControllerBrowserTest,
-                       DISABLED_DefaultContextMenu) {
-  // If ash is does not contain the relevant test controller functionality, then
-  // there's nothing to do for this test.
-  if (chromeos::LacrosService::Get()->GetInterfaceVersion(
-          crosapi::mojom::TestController::Uuid_) <
-      static_cast<int>(crosapi::mojom::TestController::MethodMinVersions::
-                           kDoesItemExistInShelfMinVersion)) {
-    LOG(WARNING) << "Unsupported ash version.";
-    return;
+ protected:
+  // If ash is does not contain the relevant test controller functionality,
+  // then there's nothing to do for this test.
+  bool IsServiceAvailable() {
+    DCHECK(IsWebAppsCrosapiEnabled());
+    auto* const service = chromeos::LacrosService::Get();
+    return service->GetInterfaceVersion(
+               crosapi::mojom::TestController::Uuid_) >=
+           static_cast<int>(crosapi::mojom::TestController::MethodMinVersions::
+                                kDoesItemExistInShelfMinVersion);
   }
+};
 
-  ASSERT_TRUE(embedded_test_server()->Start());
-
-  EXPECT_TRUE(IsWebAppsCrosapiEnabled());
-  LacrosWebAppsController lacros_web_apps_controller(profile());
-  lacros_web_apps_controller.Init();
+// Test that the default context menu for a web app has the correct items.
+IN_PROC_BROWSER_TEST_F(LacrosWebAppsControllerBrowserTest, DefaultContextMenu) {
+  if (!IsServiceAvailable())
+    GTEST_SKIP() << "Unsupported ash version.";
 
   const AppId app_id =
-      InstallPWA(embedded_test_server()->GetURL("/web_apps/basic.html"));
+      InstallPWA(https_server()->GetURL("/web_apps/basic.html"));
+  AppRegistrationWaiter(profile(), app_id).Await();
 
   // No item should exist in the shelf before the web app is launched.
   browser_test_util::WaitForShelfItem(app_id, /*exists=*/false);
 
-  crosapi::mojom::LaunchParamsPtr launch_params =
-      crosapi::mojom::LaunchParams::New();
-  launch_params->app_id = app_id;
-  launch_params->launch_source = apps::mojom::LaunchSource::kFromTest;
-  static_cast<crosapi::mojom::AppController&>(lacros_web_apps_controller)
-      .Launch(std::move(launch_params), base::DoNothing());
+  LaunchWebAppBrowser(app_id);
 
   // Wait for item to exist in shelf.
   browser_test_util::WaitForShelfItem(app_id, /*exists=*/true);
diff --git a/chrome/browser/web_applications/policy/web_app_settings_policy_handler.cc b/chrome/browser/web_applications/policy/web_app_settings_policy_handler.cc
index ac149b3..f84e282 100644
--- a/chrome/browser/web_applications/policy/web_app_settings_policy_handler.cc
+++ b/chrome/browser/web_applications/policy/web_app_settings_policy_handler.cc
@@ -35,7 +35,8 @@
   if (!policy_entry)
     return true;
 
-  const auto& web_apps_list = policy_entry->value()->GetList();
+  const auto& web_apps_list =
+      policy_entry->value(base::Value::Type::LIST)->GetList();
   const auto it = std::find_if(
       web_apps_list.begin(), web_apps_list.end(), [](const base::Value& entry) {
         return entry.FindKey(kManifestId)->GetString() == kWildcard;
diff --git a/chrome/browser/web_applications/test/app_registration_waiter.cc b/chrome/browser/web_applications/test/app_registration_waiter.cc
new file mode 100644
index 0000000..7e46c1f
--- /dev/null
+++ b/chrome/browser/web_applications/test/app_registration_waiter.cc
@@ -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.
+
+#include "chrome/browser/web_applications/test/app_registration_waiter.h"
+
+#include "chrome/browser/apps/app_service/app_service_proxy.h"
+#include "chrome/browser/apps/app_service/app_service_proxy_factory.h"
+
+namespace web_app {
+
+AppRegistrationWaiter::AppRegistrationWaiter(Profile* profile,
+                                             const AppId& app_id)
+    : app_id_(app_id) {
+  apps::AppRegistryCache& cache =
+      apps::AppServiceProxyFactory::GetForProfile(profile)->AppRegistryCache();
+  Observe(&cache);
+  if (cache.ForOneApp(app_id, [](const apps::AppUpdate&) {}))
+    run_loop_.Quit();
+}
+AppRegistrationWaiter::~AppRegistrationWaiter() = default;
+
+void AppRegistrationWaiter::Await() {
+  run_loop_.Run();
+}
+
+void AppRegistrationWaiter::OnAppUpdate(const apps::AppUpdate& update) {
+  if (update.AppId() == app_id_)
+    run_loop_.Quit();
+}
+void AppRegistrationWaiter::OnAppRegistryCacheWillBeDestroyed(
+    apps::AppRegistryCache* cache) {
+  Observe(nullptr);
+}
+
+}  // namespace web_app
diff --git a/chrome/browser/web_applications/test/app_registration_waiter.h b/chrome/browser/web_applications/test/app_registration_waiter.h
new file mode 100644
index 0000000..ba4cf37e
--- /dev/null
+++ b/chrome/browser/web_applications/test/app_registration_waiter.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 CHROME_BROWSER_WEB_APPLICATIONS_TEST_APP_REGISTRATION_WAITER_H_
+#define CHROME_BROWSER_WEB_APPLICATIONS_TEST_APP_REGISTRATION_WAITER_H_
+
+#include "base/run_loop.h"
+#include "chrome/browser/web_applications/web_app_id.h"
+#include "components/services/app_service/public/cpp/app_registry_cache.h"
+#include "components/services/app_service/public/cpp/app_update.h"
+
+class Profile;
+
+namespace web_app {
+
+class AppRegistrationWaiter : public apps::AppRegistryCache::Observer {
+ public:
+  AppRegistrationWaiter(Profile* profile, const AppId& app_id);
+  ~AppRegistrationWaiter() override;
+
+  void Await();
+
+ private:
+  // apps::AppRegistryCache::Observer:
+  void OnAppUpdate(const apps::AppUpdate& update) override;
+  void OnAppRegistryCacheWillBeDestroyed(
+      apps::AppRegistryCache* cache) override;
+
+  const AppId app_id_;
+  base::RunLoop run_loop_;
+};
+
+}  // namespace web_app
+
+#endif  // CHROME_BROWSER_WEB_APPLICATIONS_TEST_APP_REGISTRATION_WAITER_H_
diff --git a/chrome/build/linux.pgo.txt b/chrome/build/linux.pgo.txt
index 1994a6b..8df3ae4 100644
--- a/chrome/build/linux.pgo.txt
+++ b/chrome/build/linux.pgo.txt
@@ -1 +1 @@
-chrome-linux-main-1648231192-00a79f1e146c27c001ea594e843d589ee70a8fec.profdata
+chrome-linux-main-1648252801-be15c64e80010c3c479f86d173140c681bb38a47.profdata
diff --git a/chrome/build/mac-arm.pgo.txt b/chrome/build/mac-arm.pgo.txt
index 36b2253..bf99a74 100644
--- a/chrome/build/mac-arm.pgo.txt
+++ b/chrome/build/mac-arm.pgo.txt
@@ -1 +1 @@
-chrome-mac-arm-main-1648231192-3c2490485831b0f09c86bd23d8256a0603a99ece.profdata
+chrome-mac-arm-main-1648252801-fed7e9ae334d0c3d1b2adeed1619fbba62a46d26.profdata
diff --git a/chrome/build/mac.pgo.txt b/chrome/build/mac.pgo.txt
index 9f484b0e..44f020de 100644
--- a/chrome/build/mac.pgo.txt
+++ b/chrome/build/mac.pgo.txt
@@ -1 +1 @@
-chrome-mac-main-1648231192-d0c8cc3c32150d711191de27df7aad35f66e8ced.profdata
+chrome-mac-main-1648252801-e62c0cb609be3f897e97c5d5c9b3c0ed1ecb6da4.profdata
diff --git a/chrome/build/win32.pgo.txt b/chrome/build/win32.pgo.txt
index 86373fb..58d8f0c 100644
--- a/chrome/build/win32.pgo.txt
+++ b/chrome/build/win32.pgo.txt
@@ -1 +1 @@
-chrome-win32-main-1648231192-59d0208f446bf0d15979bd5241c3f83c74869aee.profdata
+chrome-win32-main-1648252801-4b1b59ef8a58544a6771e1ede132941b688665a5.profdata
diff --git a/chrome/build/win64.pgo.txt b/chrome/build/win64.pgo.txt
index 3bb852b..99bec3f 100644
--- a/chrome/build/win64.pgo.txt
+++ b/chrome/build/win64.pgo.txt
@@ -1 +1 @@
-chrome-win64-main-1648231192-cf18dea6c5392b66b22b709e088251e1b107f6c7.profdata
+chrome-win64-main-1648252801-68a4bbc7d1f1f5fb5aaaa0b064a3623fbf454ed2.profdata
diff --git a/chrome/test/data/webui/settings/chromeos/multidevice_permissions_setup_dialog_tests.js b/chrome/test/data/webui/settings/chromeos/multidevice_permissions_setup_dialog_tests.js
index b5a4ff33..302a863 100644
--- a/chrome/test/data/webui/settings/chromeos/multidevice_permissions_setup_dialog_tests.js
+++ b/chrome/test/data/webui/settings/chromeos/multidevice_permissions_setup_dialog_tests.js
@@ -6,7 +6,7 @@
 // #import 'chrome://os-settings/chromeos/os_settings.js';
 
 // #import {assert} from 'chrome://resources/js/assert.m.js';
-// #import {assertEquals, assertFalse, assertNotEquals, assertTrue} from '../../chai_assert.js';
+// #import {assertEquals, assertFalse, assertNotEquals, assertTrue, assertArrayEquals} from '../../chai_assert.js';
 // #import {flush} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
 // #import {TestMultideviceBrowserProxy} from './test_multidevice_browser_proxy.m.js';
 // #import {MultiDeviceBrowserProxyImpl, PermissionsSetupStatus, SetupFlowStatus} from 'chrome://os-settings/chromeos/os_settings.js';
@@ -32,7 +32,7 @@
   /**
    * @param {PermissionsSetupStatus} status
    */
-  function simulateStatusChanged(status) {
+  function simulateNotificationStatusChanged(status) {
     cr.webUIListenerCallback(
         'settings.onNotificationAccessSetupStatusChanged', status);
     Polymer.dom.flush();
@@ -46,6 +46,12 @@
     Polymer.dom.flush();
   }
 
+  function simulateCombinedStatusChanged(status) {
+    cr.webUIListenerCallback(
+        'settings.onCombinedAccessSetupStatusChanged', status);
+    Polymer.dom.flush();
+  }
+
   /**
    * @param {SetupFlowStatus} status
    */
@@ -71,6 +77,7 @@
       showCameraRoll: false,
       showNotifications: true,
       showAppStreaming: false,
+      combinedSetupSupported: false
     });
     Polymer.dom.flush();
 
@@ -85,7 +92,8 @@
     assertTrue(
         isExpectedFlowState(SetupFlowStatus.WAIT_FOR_PHONE_NOTIFICATION));
 
-    simulateStatusChanged(PermissionsSetupStatus.CONNECTION_REQUESTED);
+    simulateNotificationStatusChanged(
+        PermissionsSetupStatus.CONNECTION_REQUESTED);
     assertFalse(!!dialogBody.querySelector('#start-setup-description'));
     assertFalse(!!buttonContainer.querySelector('#learnMore'));
     assertTrue(!!buttonContainer.querySelector('#cancelButton'));
@@ -93,7 +101,7 @@
     assertFalse(!!buttonContainer.querySelector('#doneButton'));
     assertFalse(!!buttonContainer.querySelector('#tryAgainButton'));
 
-    simulateStatusChanged(PermissionsSetupStatus.CONNECTING);
+    simulateNotificationStatusChanged(PermissionsSetupStatus.CONNECTING);
     assertFalse(!!dialogBody.querySelector('#start-setup-description'));
     assertFalse(!!buttonContainer.querySelector('#learnMore'));
     assertTrue(!!buttonContainer.querySelector('#cancelButton'));
@@ -101,7 +109,7 @@
     assertFalse(!!buttonContainer.querySelector('#doneButton'));
     assertFalse(!!buttonContainer.querySelector('#tryAgainButton'));
 
-    simulateStatusChanged(
+    simulateNotificationStatusChanged(
         PermissionsSetupStatus.SENT_MESSAGE_TO_PHONE_AND_WAITING_FOR_RESPONSE);
     assertFalse(!!dialogBody.querySelector('#start-setup-description'));
     assertTrue(!!buttonContainer.querySelector('#learnMore'));
@@ -112,7 +120,8 @@
     assertFalse(!!buttonContainer.querySelector('#tryAgainButton'));
 
     assertEquals(browserProxy.getCallCount('setFeatureEnabledState'), 0);
-    simulateStatusChanged(PermissionsSetupStatus.COMPLETED_SUCCESSFULLY);
+    simulateNotificationStatusChanged(
+        PermissionsSetupStatus.COMPLETED_SUCCESSFULLY);
     assertFalse(!!dialogBody.querySelector('#start-setup-description'));
     assertFalse(!!buttonContainer.querySelector('#learnMore'));
     assertFalse(!!buttonContainer.querySelector('#cancelButton'));
@@ -135,6 +144,7 @@
       showCameraRoll: false,
       showNotifications: true,
       showAppStreaming: false,
+      combinedSetupSupported: false
     });
     Polymer.dom.flush();
 
@@ -147,7 +157,7 @@
     assertTrue(
         isExpectedFlowState(SetupFlowStatus.WAIT_FOR_PHONE_NOTIFICATION));
 
-    simulateStatusChanged(PermissionsSetupStatus.CONNECTING);
+    simulateNotificationStatusChanged(PermissionsSetupStatus.CONNECTING);
 
     assertTrue(!!buttonContainer.querySelector('#cancelButton'));
     assertFalse(!!buttonContainer.querySelector('#getStartedButton'));
@@ -165,6 +175,7 @@
       showCameraRoll: false,
       showNotifications: true,
       showAppStreaming: false,
+      combinedSetupSupported: false
     });
     Polymer.dom.flush();
 
@@ -177,7 +188,8 @@
     assertTrue(
         isExpectedFlowState(SetupFlowStatus.WAIT_FOR_PHONE_NOTIFICATION));
 
-    simulateStatusChanged(PermissionsSetupStatus.TIMED_OUT_CONNECTING);
+    simulateNotificationStatusChanged(
+        PermissionsSetupStatus.TIMED_OUT_CONNECTING);
 
     assertTrue(!!buttonContainer.querySelector('#cancelButton'));
     assertFalse(!!buttonContainer.querySelector('#getStartedButton'));
@@ -194,7 +206,8 @@
     assertFalse(!!buttonContainer.querySelector('#doneButton'));
     assertFalse(!!buttonContainer.querySelector('#tryAgainButton'));
 
-    simulateStatusChanged(PermissionsSetupStatus.CONNECTION_DISCONNECTED);
+    simulateNotificationStatusChanged(
+        PermissionsSetupStatus.CONNECTION_DISCONNECTED);
 
     assertTrue(!!buttonContainer.querySelector('#cancelButton'));
     assertFalse(!!buttonContainer.querySelector('#getStartedButton'));
@@ -212,6 +225,7 @@
       showCameraRoll: false,
       showNotifications: true,
       showAppStreaming: false,
+      combinedSetupSupported: false
     });
     Polymer.dom.flush();
 
@@ -225,7 +239,7 @@
     assertTrue(
         isExpectedFlowState(SetupFlowStatus.WAIT_FOR_PHONE_NOTIFICATION));
 
-    simulateStatusChanged(
+    simulateNotificationStatusChanged(
         PermissionsSetupStatus.NOTIFICATION_ACCESS_PROHIBITED);
 
     assertFalse(!!buttonContainer.querySelector('#cancelButton'));
@@ -244,6 +258,7 @@
       showCameraRoll: false,
       showNotifications: false,
       showAppStreaming: true,
+      combinedSetupSupported: false
     });
     Polymer.dom.flush();
 
@@ -307,6 +322,7 @@
       showCameraRoll: false,
       showNotifications: false,
       showAppStreaming: true,
+      combinedSetupSupported: false
     });
     Polymer.dom.flush();
 
@@ -336,6 +352,7 @@
       showCameraRoll: false,
       showNotifications: false,
       showAppStreaming: true,
+      combinedSetupSupported: false
     });
     Polymer.dom.flush();
 
@@ -382,6 +399,7 @@
       showCameraRoll: false,
       showNotifications: true,
       showAppStreaming: true,
+      combinedSetupSupported: false
     });
     Polymer.dom.flush();
 
@@ -396,7 +414,7 @@
     assertTrue(
         isExpectedFlowState(SetupFlowStatus.WAIT_FOR_PHONE_NOTIFICATION));
 
-    simulateStatusChanged(
+    simulateNotificationStatusChanged(
         PermissionsSetupStatus.SENT_MESSAGE_TO_PHONE_AND_WAITING_FOR_RESPONSE);
     assertFalse(!!dialogBody.querySelector('#start-setup-description'));
     assertTrue(!!buttonContainer.querySelector('#learnMore'));
@@ -407,7 +425,8 @@
     assertFalse(!!buttonContainer.querySelector('#tryAgainButton'));
 
     assertEquals(browserProxy.getCallCount('setFeatureEnabledState'), 0);
-    simulateStatusChanged(PermissionsSetupStatus.COMPLETED_SUCCESSFULLY);
+    simulateNotificationStatusChanged(
+        PermissionsSetupStatus.COMPLETED_SUCCESSFULLY);
     assertFalse(!!dialogBody.querySelector('#start-setup-description'));
     assertFalse(!!buttonContainer.querySelector('#learnMore'));
     assertTrue(!!buttonContainer.querySelector('#cancelButton'));
@@ -444,6 +463,7 @@
       showCameraRoll: false,
       showNotifications: true,
       showAppStreaming: true,
+      combinedSetupSupported: false
     });
     Polymer.dom.flush();
 
@@ -466,6 +486,7 @@
       showCameraRoll: false,
       showNotifications: true,
       showAppStreaming: true,
+      combinedSetupSupported: false
     });
     Polymer.dom.flush();
 
@@ -481,6 +502,7 @@
       showCameraRoll: false,
       showNotifications: true,
       showAppStreaming: true,
+      combinedSetupSupported: false
     });
     Polymer.dom.flush();
 
@@ -496,6 +518,7 @@
       showCameraRoll: false,
       showNotifications: true,
       showAppStreaming: true,
+      combinedSetupSupported: false
     });
     Polymer.dom.flush();
 
@@ -511,6 +534,7 @@
       showCameraRoll: false,
       showNotifications: true,
       showAppStreaming: false,
+      combinedSetupSupported: false
     });
     Polymer.dom.flush();
 
@@ -530,6 +554,7 @@
           showCameraRoll: false,
           showNotifications: true,
           showAppStreaming: false,
+          combinedSetupSupported: false
         });
         Polymer.dom.flush();
 
@@ -542,4 +567,145 @@
         assertTrue(
             isExpectedFlowState(SetupFlowStatus.WAIT_FOR_PHONE_NOTIFICATION));
       });
+
+  test('Test Camera Roll setup success flow', async () => {
+    permissionsSetupDialog.setProperties({
+      showCameraRoll: true,
+      showNotifications: false,
+      showAppStreaming: false,
+      combinedSetupSupported: true
+    });
+    Polymer.dom.flush();
+
+    assertTrue(!!dialogBody.querySelector('#start-setup-description'));
+    assertTrue(!!buttonContainer.querySelector('#learnMore'));
+    assertTrue(!!buttonContainer.querySelector('#cancelButton'));
+    assertTrue(!!buttonContainer.querySelector('#getStartedButton'));
+    assertFalse(!!buttonContainer.querySelector('#doneButton'));
+    assertFalse(!!buttonContainer.querySelector('#tryAgainButton'));
+    buttonContainer.querySelector('#getStartedButton').click();
+    assertEquals(browserProxy.getCallCount('attemptCombinedFeatureSetup'), 1);
+    assertArrayEquals(
+        [true, false], browserProxy.getArgs('attemptCombinedFeatureSetup')[0]);
+    assertTrue(isExpectedFlowState(SetupFlowStatus.WAIT_FOR_PHONE_COMBINED));
+
+    simulateCombinedStatusChanged(PermissionsSetupStatus.CONNECTION_REQUESTED);
+    assertFalse(!!dialogBody.querySelector('#start-setup-description'));
+    assertFalse(!!buttonContainer.querySelector('#learnMore'));
+    assertTrue(!!buttonContainer.querySelector('#cancelButton'));
+    assertFalse(!!buttonContainer.querySelector('#getStartedButton'));
+    assertFalse(!!buttonContainer.querySelector('#doneButton'));
+    assertFalse(!!buttonContainer.querySelector('#tryAgainButton'));
+
+    simulateCombinedStatusChanged(PermissionsSetupStatus.CONNECTING);
+    assertFalse(!!dialogBody.querySelector('#start-setup-description'));
+    assertFalse(!!buttonContainer.querySelector('#learnMore'));
+    assertTrue(!!buttonContainer.querySelector('#cancelButton'));
+    assertFalse(!!buttonContainer.querySelector('#getStartedButton'));
+    assertFalse(!!buttonContainer.querySelector('#doneButton'));
+    assertFalse(!!buttonContainer.querySelector('#tryAgainButton'));
+
+    simulateCombinedStatusChanged(
+        PermissionsSetupStatus.SENT_MESSAGE_TO_PHONE_AND_WAITING_FOR_RESPONSE);
+    assertFalse(!!dialogBody.querySelector('#start-setup-description'));
+    assertTrue(!!buttonContainer.querySelector('#learnMore'));
+    assertTrue(!!buttonContainer.querySelector('#cancelButton'));
+    assertFalse(!!buttonContainer.querySelector('#getStartedButton'));
+    assertTrue(!!buttonContainer.querySelector('#doneButton'));
+    assertTrue(buttonContainer.querySelector('#doneButton').disabled);
+    assertFalse(!!buttonContainer.querySelector('#tryAgainButton'));
+
+    assertEquals(browserProxy.getCallCount('setFeatureEnabledState'), 0);
+    simulateCombinedStatusChanged(
+        PermissionsSetupStatus.COMPLETED_SUCCESSFULLY);
+    assertFalse(!!dialogBody.querySelector('#start-setup-description'));
+    assertFalse(!!buttonContainer.querySelector('#learnMore'));
+    assertFalse(!!buttonContainer.querySelector('#cancelButton'));
+    assertFalse(!!buttonContainer.querySelector('#getStartedButton'));
+    assertTrue(!!buttonContainer.querySelector('#doneButton'));
+    assertFalse(buttonContainer.querySelector('#doneButton').disabled);
+    assertFalse(!!buttonContainer.querySelector('#tryAgainButton'));
+
+    // The feature becomes enabled when the status becomes
+    // PermissionsSetupStatus.COMPLETED_SUCCESSFULLY.
+    assertEquals(browserProxy.getCallCount('setFeatureEnabledState'), 1);
+
+    assertTrue(permissionsSetupDialog.$$('#dialog').open);
+    buttonContainer.querySelector('#doneButton').click();
+    assertFalse(permissionsSetupDialog.$$('#dialog').open);
+  });
+
+  test(
+      'Test Camera Roll and Notifications combined setup success flow',
+      async () => {
+        permissionsSetupDialog.setProperties({
+          showCameraRoll: true,
+          showNotifications: true,
+          showAppStreaming: false,
+          combinedSetupSupported: true
+        });
+        Polymer.dom.flush();
+
+        assertTrue(!!dialogBody.querySelector('#start-setup-description'));
+        assertTrue(!!buttonContainer.querySelector('#learnMore'));
+        assertTrue(!!buttonContainer.querySelector('#cancelButton'));
+        assertTrue(!!buttonContainer.querySelector('#getStartedButton'));
+        assertFalse(!!buttonContainer.querySelector('#doneButton'));
+        assertFalse(!!buttonContainer.querySelector('#tryAgainButton'));
+        buttonContainer.querySelector('#getStartedButton').click();
+        assertEquals(
+            browserProxy.getCallCount('attemptCombinedFeatureSetup'), 1);
+        assertArrayEquals(
+            [true, true],
+            browserProxy.getArgs('attemptCombinedFeatureSetup')[0]);
+        assertTrue(
+            isExpectedFlowState(SetupFlowStatus.WAIT_FOR_PHONE_COMBINED));
+
+        simulateCombinedStatusChanged(
+            PermissionsSetupStatus.CONNECTION_REQUESTED);
+        assertFalse(!!dialogBody.querySelector('#start-setup-description'));
+        assertFalse(!!buttonContainer.querySelector('#learnMore'));
+        assertTrue(!!buttonContainer.querySelector('#cancelButton'));
+        assertFalse(!!buttonContainer.querySelector('#getStartedButton'));
+        assertFalse(!!buttonContainer.querySelector('#doneButton'));
+        assertFalse(!!buttonContainer.querySelector('#tryAgainButton'));
+
+        simulateCombinedStatusChanged(PermissionsSetupStatus.CONNECTING);
+        assertFalse(!!dialogBody.querySelector('#start-setup-description'));
+        assertFalse(!!buttonContainer.querySelector('#learnMore'));
+        assertTrue(!!buttonContainer.querySelector('#cancelButton'));
+        assertFalse(!!buttonContainer.querySelector('#getStartedButton'));
+        assertFalse(!!buttonContainer.querySelector('#doneButton'));
+        assertFalse(!!buttonContainer.querySelector('#tryAgainButton'));
+
+        simulateCombinedStatusChanged(
+            PermissionsSetupStatus
+                .SENT_MESSAGE_TO_PHONE_AND_WAITING_FOR_RESPONSE);
+        assertFalse(!!dialogBody.querySelector('#start-setup-description'));
+        assertTrue(!!buttonContainer.querySelector('#learnMore'));
+        assertTrue(!!buttonContainer.querySelector('#cancelButton'));
+        assertFalse(!!buttonContainer.querySelector('#getStartedButton'));
+        assertTrue(!!buttonContainer.querySelector('#doneButton'));
+        assertTrue(buttonContainer.querySelector('#doneButton').disabled);
+        assertFalse(!!buttonContainer.querySelector('#tryAgainButton'));
+
+        assertEquals(browserProxy.getCallCount('setFeatureEnabledState'), 0);
+        simulateCombinedStatusChanged(
+            PermissionsSetupStatus.COMPLETED_SUCCESSFULLY);
+        assertFalse(!!dialogBody.querySelector('#start-setup-description'));
+        assertFalse(!!buttonContainer.querySelector('#learnMore'));
+        assertFalse(!!buttonContainer.querySelector('#cancelButton'));
+        assertFalse(!!buttonContainer.querySelector('#getStartedButton'));
+        assertTrue(!!buttonContainer.querySelector('#doneButton'));
+        assertFalse(buttonContainer.querySelector('#doneButton').disabled);
+        assertFalse(!!buttonContainer.querySelector('#tryAgainButton'));
+
+        // The features become enabled when the status becomes
+        // PermissionsSetupStatus.COMPLETED_SUCCESSFULLY.
+        assertEquals(browserProxy.getCallCount('setFeatureEnabledState'), 2);
+
+        assertTrue(permissionsSetupDialog.$$('#dialog').open);
+        buttonContainer.querySelector('#doneButton').click();
+        assertFalse(permissionsSetupDialog.$$('#dialog').open);
+      });
 });
diff --git a/chrome/test/data/webui/settings/chromeos/test_multidevice_browser_proxy.js b/chrome/test/data/webui/settings/chromeos/test_multidevice_browser_proxy.js
index b871dbd..d60e86ddc 100644
--- a/chrome/test/data/webui/settings/chromeos/test_multidevice_browser_proxy.js
+++ b/chrome/test/data/webui/settings/chromeos/test_multidevice_browser_proxy.js
@@ -60,6 +60,8 @@
         'cancelNotificationSetup',
         'attemptAppsSetup',
         'cancelAppsSetup',
+        'attemptCombinedFeatureSetup',
+        'cancelCombinedFeatureSetup',
       ]);
       this.data = createFakePageContentData(
           settings.MultiDeviceSettingsMode.NO_HOST_SET);
@@ -138,6 +140,17 @@
       this.methodCalled('cancelAppsSetup');
     }
 
+    /** @override */
+    attemptCombinedFeatureSetup(cameraRoll, notifications) {
+      this.methodCalled(
+          'attemptCombinedFeatureSetup', [cameraRoll, notifications]);
+    }
+
+    /** @override */
+    cancelCombinedFeatureSetup() {
+      this.methodCalled('cancelCombinedFeatureSetup');
+    }
+
     /**
      * @param {settings.MultiDeviceFeature} state
      */
diff --git a/chrome/test/data/webui/signin/enterprise_profile_welcome_test.ts b/chrome/test/data/webui/signin/enterprise_profile_welcome_test.ts
index 3d38d88..90befe3a2 100644
--- a/chrome/test/data/webui/signin/enterprise_profile_welcome_test.ts
+++ b/chrome/test/data/webui/signin/enterprise_profile_welcome_test.ts
@@ -5,10 +5,10 @@
 import 'chrome://enterprise-profile-welcome/enterprise_profile_welcome_app.js';
 
 import {EnterpriseProfileWelcomeAppElement} from 'chrome://enterprise-profile-welcome/enterprise_profile_welcome_app.js';
-
 import {EnterpriseProfileWelcomeBrowserProxyImpl} from 'chrome://enterprise-profile-welcome/enterprise_profile_welcome_browser_proxy.js';
+import {CrCheckboxElement} from 'chrome://resources/cr_elements/cr_checkbox/cr_checkbox.m.js';
 import {webUIListenerCallback} from 'chrome://resources/js/cr.m.js';
-
+import {loadTimeData} from 'chrome://resources/js/load_time_data.m.js';
 import {assertEquals, assertFalse, assertTrue} from 'chrome://webui-test/chai_assert.js';
 import {isChildVisible, waitAfterNextRender} from 'chrome://webui-test/test_util.js';
 
@@ -69,6 +69,37 @@
     await browserProxy.whenCalled('cancel');
   });
 
+  test('linkData', async function() {
+    assertTrue(isChildVisible(app, '#proceedButton'));
+    assertFalse(isChildVisible(app, '#linkData'));
+
+    loadTimeData.overrideValues({'showLinkDataCheckbox': true});
+
+    document.body.innerHTML = '';
+    app = document.createElement('enterprise-profile-welcome-app');
+    document.body.appendChild(app);
+    await waitAfterNextRender(app);
+    await browserProxy.whenCalled('initialized');
+
+    assertTrue(isChildVisible(app, '#proceedButton'));
+    assertTrue(isChildVisible(app, '#linkData'));
+
+    const linkDataCheckbox: CrCheckboxElement =
+        app.shadowRoot!.querySelector('#linkData')!;
+    assertEquals(
+        app.i18n('linkDataText'), linkDataCheckbox.textContent!.trim());
+    assertFalse(linkDataCheckbox.checked);
+
+    linkDataCheckbox.click();
+
+    await waitAfterNextRender(app.$.proceedButton);
+
+    assertTrue(linkDataCheckbox.checked);
+    assertEquals(
+        app.i18n('proceedAlternateLabel'),
+        app.$.proceedButton.textContent!.trim());
+  });
+
   test('onProfileInfoChanged', function() {
     // Helper to test all the text values in the UI.
     function checkTextValues(
diff --git a/chromeos/network/metrics/network_metrics_helper.cc b/chromeos/network/metrics/network_metrics_helper.cc
index 72613a7..37d25e4d 100644
--- a/chromeos/network/metrics/network_metrics_helper.cc
+++ b/chromeos/network/metrics/network_metrics_helper.cc
@@ -41,6 +41,7 @@
 const char kVPN[] = "VPN";
 const char kVPNBuiltIn[] = "VPN.TypeBuiltIn";
 const char kVPNThirdParty[] = "VPN.TypeThirdParty";
+const char kVPNUnknown[] = "VPN.TypeUnknown";
 
 const char kWifi[] = "WiFi";
 const char kWifiOpen[] = "WiFi.SecurityOpen";
@@ -123,12 +124,14 @@
   if (vpn_provider_type == shill::kProviderThirdPartyVpn ||
       vpn_provider_type == shill::kProviderArcVpn) {
     vpn_histograms.emplace_back(kVPNThirdParty);
-  } else if (vpn_provider_type == shill::kProviderL2tpIpsec ||
+  } else if (vpn_provider_type == shill::kProviderIKEv2 ||
+             vpn_provider_type == shill::kProviderL2tpIpsec ||
              vpn_provider_type == shill::kProviderOpenVpn ||
              vpn_provider_type == shill::kProviderWireGuard) {
     vpn_histograms.emplace_back(kVPNBuiltIn);
   } else {
     NOTREACHED();
+    vpn_histograms.emplace_back(kVPNUnknown);
   }
   return vpn_histograms;
 }
diff --git a/chromeos/network/metrics/network_metrics_helper_unittest.cc b/chromeos/network/metrics/network_metrics_helper_unittest.cc
index 8255e04..d8b0521 100644
--- a/chromeos/network/metrics/network_metrics_helper_unittest.cc
+++ b/chromeos/network/metrics/network_metrics_helper_unittest.cc
@@ -14,6 +14,7 @@
 #include "chromeos/network/network_handler_test_helper.h"
 #include "chromeos/network/network_ui_data.h"
 #include "components/prefs/testing_pref_service.h"
+#include "testing/gtest/include/gtest/gtest-spi.h"
 #include "testing/gtest/include/gtest/gtest.h"
 #include "third_party/cros_system_api/dbus/service_constants.h"
 
@@ -40,6 +41,8 @@
     "Network.Ash.VPN.TypeBuiltIn.ConnectionResult.All";
 const char kVpnThirdPartyConnectResultAllHistogram[] =
     "Network.Ash.VPN.TypeThirdParty.ConnectionResult.All";
+const char kVpnUnknownConnectResultAllHistogram[] =
+    "Network.Ash.VPN.TypeUnknown.ConnectionResult.All";
 
 // LogAllConnectionResult() WiFi histograms.
 const char kWifiConnectResultAllHistogram[] =
@@ -74,6 +77,8 @@
     "Network.Ash.VPN.TypeBuiltIn.ConnectionResult.UserInitiated";
 const char kVpnThirdPartyConnectResultUserInitiatedHistogram[] =
     "Network.Ash.VPN.TypeThirdParty.ConnectionResult.UserInitiated";
+const char kVpnUnknownConnectResultUserInitiatedHistogram[] =
+    "Network.Ash.VPN.TypeUnknown.ConnectionResult.UserInitiated";
 
 // LogUserInitiatedConnectionResult() WiFi histograms.
 const char kWifiConnectResultUserInitiatedHistogram[] =
@@ -108,6 +113,8 @@
     "Network.Ash.VPN.TypeBuiltIn.DisconnectionsWithoutUserAction";
 const char kVpnThirdPartyConnectionStateHistogram[] =
     "Network.Ash.VPN.TypeThirdParty.DisconnectionsWithoutUserAction";
+const char kVpnUnknownConnectionStateHistogram[] =
+    "Network.Ash.VPN.TypeUnknown.DisconnectionsWithoutUserAction";
 
 // LogConnectionStateResult() WiFi histograms.
 const char kWifiConnectionStateHistogram[] =
@@ -152,6 +159,25 @@
 const char kTestDevicePath[] = "/device/network";
 const char kTestName[] = "network_name";
 const char kTestVpnHost[] = "test host";
+const char kTestUnknownVpn[] = "test_unknown_vpn";
+
+void LogVpnResult(const std::string& provider,
+                  base::RepeatingClosure func,
+                  bool* failed_to_log_result) {
+  ASSERT_NE(failed_to_log_result, nullptr);
+
+// Emitting a metric for an unknown VPN provider will always cause a NOTREACHED
+// to be hit. This can cause a CHECK to fail, depending on the build flags. We
+// catch any failing CHECK below by asserting that we will crash when emitting.
+#if !BUILDFLAG(ENABLE_LOG_ERROR_NOT_REACHED)
+  if (provider == kTestUnknownVpn) {
+    ASSERT_DEATH({ func.Run(); }, "");
+    *failed_to_log_result = true;
+    return;
+  }
+#endif  // !BUILDFLAG(ENABLE_LOG_ERROR_NOT_REACHED)
+  func.Run();
+}
 
 }  // namespace
 
@@ -334,19 +360,35 @@
 
 TEST_F(NetworkMetricsHelperTest, VPN) {
   const std::vector<const std::string> kProviders{{
+      shill::kProviderIKEv2,
       shill::kProviderL2tpIpsec,
       shill::kProviderArcVpn,
       shill::kProviderOpenVpn,
       shill::kProviderThirdPartyVpn,
       shill::kProviderWireGuard,
+      kTestUnknownVpn,
   }};
 
   size_t expected_all_count = 0;
   size_t expected_user_initiated_count = 0;
   size_t expected_built_in_count = 0;
   size_t expected_third_party_count = 0;
+  size_t expected_unknown_count = 0;
+
+  base::RepeatingClosure log_all_connection_result =
+      base::BindRepeating(&NetworkMetricsHelper::LogAllConnectionResult,
+                          kTestGuid, shill::kErrorNotRegistered);
+  base::RepeatingClosure log_user_initiated_connection_result =
+      base::BindRepeating(
+          &NetworkMetricsHelper::LogUserInitiatedConnectionResult, kTestGuid,
+          shill::kErrorNotRegistered);
+  base::RepeatingClosure log_connection_state_result = base::BindRepeating(
+      &NetworkMetricsHelper::LogConnectionStateResult, kTestGuid,
+      NetworkMetricsHelper::ConnectionState::kConnected);
 
   for (const auto& provider : kProviders) {
+    bool failed_to_log_result = false;
+
     shill_service_client_->AddService(kTestServicePath, kTestGuid, kTestName,
                                       shill::kTypeVPN, shill::kStateIdle,
                                       /*visible=*/true);
@@ -357,26 +399,36 @@
                                               base::Value(kTestVpnHost));
     base::RunLoop().RunUntilIdle();
 
-    if (provider == shill::kProviderThirdPartyVpn ||
-        provider == shill::kProviderArcVpn) {
-      ++expected_third_party_count;
-    } else {
-      ++expected_built_in_count;
-    }
-    ++expected_all_count;
-    ++expected_user_initiated_count;
+    LogVpnResult(provider, log_all_connection_result, &failed_to_log_result);
+    LogVpnResult(provider, log_user_initiated_connection_result,
+                 &failed_to_log_result);
+    LogVpnResult(provider, log_connection_state_result, &failed_to_log_result);
 
-    NetworkMetricsHelper::LogAllConnectionResult(kTestGuid,
-                                                 shill::kErrorNotRegistered);
+    if (!failed_to_log_result) {
+      if (provider == shill::kProviderThirdPartyVpn ||
+          provider == shill::kProviderArcVpn) {
+        ++expected_third_party_count;
+      } else if (provider == shill::kProviderIKEv2 ||
+                 provider == shill::kProviderL2tpIpsec ||
+                 provider == shill::kProviderOpenVpn ||
+                 provider == shill::kProviderWireGuard) {
+        ++expected_built_in_count;
+      } else {
+        ++expected_unknown_count;
+      }
+      ++expected_all_count;
+      ++expected_user_initiated_count;
+    }
+
     histogram_tester_->ExpectTotalCount(kVpnConnectResultAllHistogram,
                                         expected_all_count);
     histogram_tester_->ExpectTotalCount(kVpnBuiltInConnectResultAllHistogram,
                                         expected_built_in_count);
     histogram_tester_->ExpectTotalCount(kVpnThirdPartyConnectResultAllHistogram,
                                         expected_third_party_count);
+    histogram_tester_->ExpectTotalCount(kVpnUnknownConnectResultAllHistogram,
+                                        expected_unknown_count);
 
-    NetworkMetricsHelper::LogUserInitiatedConnectionResult(
-        kTestGuid, shill::kErrorNotRegistered);
     histogram_tester_->ExpectTotalCount(kVpnConnectResultUserInitiatedHistogram,
                                         expected_user_initiated_count);
     histogram_tester_->ExpectTotalCount(
@@ -385,15 +437,17 @@
     histogram_tester_->ExpectTotalCount(
         kVpnThirdPartyConnectResultUserInitiatedHistogram,
         expected_third_party_count);
+    histogram_tester_->ExpectTotalCount(
+        kVpnUnknownConnectResultUserInitiatedHistogram, expected_unknown_count);
 
-    NetworkMetricsHelper::LogConnectionStateResult(
-        kTestGuid, NetworkMetricsHelper::ConnectionState::kConnected);
     histogram_tester_->ExpectTotalCount(kVpnConnectionStateHistogram,
                                         expected_user_initiated_count);
     histogram_tester_->ExpectTotalCount(kVpnBuiltInConnectionStateHistogram,
                                         expected_built_in_count);
     histogram_tester_->ExpectTotalCount(kVpnThirdPartyConnectionStateHistogram,
                                         expected_third_party_count);
+    histogram_tester_->ExpectTotalCount(kVpnUnknownConnectionStateHistogram,
+                                        expected_unknown_count);
 
     shill_service_client_->RemoveService(kTestServicePath);
     base::RunLoop().RunUntilIdle();
diff --git a/chromeos/network/metrics/vpn_network_metrics_helper.cc b/chromeos/network/metrics/vpn_network_metrics_helper.cc
index 0678bcc..94e429b 100644
--- a/chromeos/network/metrics/vpn_network_metrics_helper.cc
+++ b/chromeos/network/metrics/vpn_network_metrics_helper.cc
@@ -20,6 +20,8 @@
 // sources of created VPNs.
 const char kVpnConfigurationSourceBucketArc[] =
     "Network.Ash.VPN.ARC.ConfigurationSource";
+const char kVpnConfigurationSourceBucketIKEv2[] =
+    "Network.Ash.VPN.IKEv2.ConfigurationSource";
 const char kVpnConfigurationSourceBucketL2tpIpsec[] =
     "Network.Ash.VPN.L2TPIPsec.ConfigurationSource";
 const char kVpnConfigurationSourceBucketOpenVpn[] =
@@ -28,10 +30,14 @@
     "Network.Ash.VPN.ThirdParty.ConfigurationSource";
 const char kVpnConfigurationSourceBucketWireGuard[] =
     "Network.Ash.VPN.WireGuard.ConfigurationSource";
+const char kVpnConfigurationSourceBucketUnknown[] =
+    "Network.Ash.VPN.Unknown.ConfigurationSource";
 
 const char* GetBucketForVpnProviderType(const std::string& vpn_provider_type) {
   if (vpn_provider_type == shill::kProviderArcVpn) {
     return kVpnConfigurationSourceBucketArc;
+  } else if (vpn_provider_type == shill::kProviderIKEv2) {
+    return kVpnConfigurationSourceBucketIKEv2;
   } else if (vpn_provider_type == shill::kProviderL2tpIpsec) {
     return kVpnConfigurationSourceBucketL2tpIpsec;
   } else if (vpn_provider_type == shill::kProviderOpenVpn) {
@@ -41,7 +47,8 @@
   } else if (vpn_provider_type == shill::kProviderWireGuard) {
     return kVpnConfigurationSourceBucketWireGuard;
   }
-  return nullptr;
+  NOTREACHED();
+  return kVpnConfigurationSourceBucketUnknown;
 }
 
 }  // namespace
@@ -71,10 +78,7 @@
   const char* vpn_provider_type_bucket =
       GetBucketForVpnProviderType(network_state->GetVpnProviderType());
 
-  if (!vpn_provider_type_bucket) {
-    NOTREACHED();
-    return;
-  }
+  DCHECK(vpn_provider_type_bucket);
 
   base::UmaHistogramEnumeration(
       vpn_provider_type_bucket,
diff --git a/chromeos/network/metrics/vpn_network_metrics_helper_unittest.cc b/chromeos/network/metrics/vpn_network_metrics_helper_unittest.cc
index e77c2a72..e5bae2d 100644
--- a/chromeos/network/metrics/vpn_network_metrics_helper_unittest.cc
+++ b/chromeos/network/metrics/vpn_network_metrics_helper_unittest.cc
@@ -19,6 +19,7 @@
 #include "chromeos/network/network_ui_data.h"
 #include "chromeos/network/shill_property_util.h"
 #include "components/prefs/testing_pref_service.h"
+#include "testing/gtest/include/gtest/gtest-spi.h"
 #include "testing/gtest/include/gtest/gtest.h"
 #include "third_party/cros_system_api/dbus/service_constants.h"
 
@@ -28,6 +29,8 @@
 
 const char kVpnHistogramConfigurationSourceArc[] =
     "Network.Ash.VPN.ARC.ConfigurationSource";
+const char kVpnHistogramConfigurationSourceIKEv2[] =
+    "Network.Ash.VPN.IKEv2.ConfigurationSource";
 const char kVpnHistogramConfigurationSourceL2tpIpsec[] =
     "Network.Ash.VPN.L2TPIPsec.ConfigurationSource";
 const char kVpnHistogramConfigurationSourceOpenVpn[] =
@@ -36,11 +39,45 @@
     "Network.Ash.VPN.ThirdParty.ConfigurationSource";
 const char kVpnHistogramConfigurationSourceWireGuard[] =
     "Network.Ash.VPN.WireGuard.ConfigurationSource";
+const char kVpnHistogramConfigurationSourceUnknown[] =
+    "Network.Ash.VPN.Unknown.ConfigurationSource";
+
+const char kTestUnknownVpn[] = "test_unknown_vpn";
 
 void ErrorCallback(const std::string& error_name) {
   ADD_FAILURE() << "Unexpected error: " << error_name;
 }
 
+// Helper function to create a VPN network using NetworkConfigurationHandler.
+void CreateTestShillConfiguration(const std::string& vpn_provider_type,
+                                  bool is_managed) {
+  base::Value properties(base::Value::Type::DICTIONARY);
+
+  properties.SetKey(shill::kGuidProperty, base::Value("vpn_guid"));
+  properties.SetKey(shill::kTypeProperty, base::Value(shill::kTypeVPN));
+  properties.SetKey(shill::kStateProperty, base::Value(shill::kStateIdle));
+  properties.SetKey(shill::kProviderHostProperty, base::Value("vpn_host"));
+  properties.SetKey(shill::kProviderTypeProperty,
+                    base::Value(vpn_provider_type));
+  properties.SetKey(shill::kProfileProperty,
+                    base::Value(NetworkProfileHandler::GetSharedProfilePath()));
+
+  if (is_managed) {
+    properties.SetKey(shill::kONCSourceProperty,
+                      base::Value(shill::kONCSourceDevicePolicy));
+    std::unique_ptr<NetworkUIData> ui_data = NetworkUIData::CreateFromONC(
+        ::onc::ONCSource::ONC_SOURCE_DEVICE_POLICY);
+    properties.SetKey(shill::kUIDataProperty,
+                      base::Value(ui_data->GetAsJson()));
+  }
+
+  NetworkHandler::Get()
+      ->network_configuration_handler()
+      ->CreateShillConfiguration(properties, base::DoNothing(),
+                                 base::BindOnce(&ErrorCallback));
+  base::RunLoop().RunUntilIdle();
+}
+
 }  // namespace
 
 class VpnNetworkMetricsHelperTest : public testing::Test {
@@ -68,37 +105,6 @@
     base::RunLoop().RunUntilIdle();
   }
 
-  // Helper function to create a VPN network using NetworkConfigurationHandler.
-  void CreateTestShillConfiguration(const char* vpn_provider_type,
-                                    bool is_managed) {
-    base::Value properties(base::Value::Type::DICTIONARY);
-
-    properties.SetKey(shill::kGuidProperty, base::Value("vpn_guid"));
-    properties.SetKey(shill::kTypeProperty, base::Value(shill::kTypeVPN));
-    properties.SetKey(shill::kStateProperty, base::Value(shill::kStateIdle));
-    properties.SetKey(shill::kProviderHostProperty, base::Value("vpn_host"));
-    properties.SetKey(shill::kProviderTypeProperty,
-                      base::Value(vpn_provider_type));
-    properties.SetKey(
-        shill::kProfileProperty,
-        base::Value(NetworkProfileHandler::GetSharedProfilePath()));
-
-    if (is_managed) {
-      properties.SetKey(shill::kONCSourceProperty,
-                        base::Value(shill::kONCSourceDevicePolicy));
-      std::unique_ptr<NetworkUIData> ui_data = NetworkUIData::CreateFromONC(
-          ::onc::ONCSource::ONC_SOURCE_DEVICE_POLICY);
-      properties.SetKey(shill::kUIDataProperty,
-                        base::Value(ui_data->GetAsJson()));
-    }
-
-    NetworkHandler::Get()
-        ->network_configuration_handler()
-        ->CreateShillConfiguration(properties, base::DoNothing(),
-                                   base::BindOnce(&ErrorCallback));
-    base::RunLoop().RunUntilIdle();
-  }
-
   void ExpectConfigurationSourceCounts(const char* histogram,
                                        size_t manual_count,
                                        size_t policy_count) {
@@ -122,8 +128,9 @@
 };
 
 TEST_F(VpnNetworkMetricsHelperTest, LogVpnVPNConfigurationSource) {
-  const std::vector<std::pair<const char*, const char*>>
+  const std::vector<std::pair<const std::string, const char*>>
       kProvidersAndHistograms{{
+          {shill::kProviderIKEv2, kVpnHistogramConfigurationSourceIKEv2},
           {shill::kProviderL2tpIpsec,
            kVpnHistogramConfigurationSourceL2tpIpsec},
           {shill::kProviderArcVpn, kVpnHistogramConfigurationSourceArc},
@@ -132,12 +139,36 @@
            kVpnHistogramConfigurationSourceThirdParty},
           {shill::kProviderWireGuard,
            kVpnHistogramConfigurationSourceWireGuard},
+          {kTestUnknownVpn, kVpnHistogramConfigurationSourceUnknown},
       }};
 
   for (const auto& it : kProvidersAndHistograms) {
-    ExpectConfigurationSourceCounts(it.first, /*manual_count=*/0,
+    ExpectConfigurationSourceCounts(it.first.c_str(), /*manual_count=*/0,
                                     /*policy_count=*/0);
 
+// Emitting a metric for an unknown VPN provider will always cause a NOTREACHED
+// to be hit. This can cause a CHECK to fail, depending on the build flags. We
+// catch any failing CHECK below by asserting that we will crash when emitting.
+#if !BUILDFLAG(ENABLE_LOG_ERROR_NOT_REACHED)
+    if (it.first == kTestUnknownVpn) {
+      ASSERT_DEATH(
+          {
+            CreateTestShillConfiguration(kTestUnknownVpn, /*is_managed=*/false);
+          },
+          "");
+      ClearServices();
+      ASSERT_DEATH(
+          {
+            CreateTestShillConfiguration(kTestUnknownVpn, /*is_managed=*/true);
+          },
+          "");
+      ClearServices();
+      ExpectConfigurationSourceCounts(it.second, /*manual_count=*/0,
+                                      /*policy_count=*/0);
+      continue;
+    }
+#endif  // !BUILDFLAG(ENABLE_LOG_ERROR_NOT_REACHED)
+
     CreateTestShillConfiguration(it.first, /*is_managed=*/false);
     ExpectConfigurationSourceCounts(it.second, /*manual_count=*/1,
                                     /*policy_count=*/0);
diff --git a/chromeos/services/libassistant/grpc/assistant_client.h b/chromeos/services/libassistant/grpc/assistant_client.h
index 3abc1d6..7b8cd88ea 100644
--- a/chromeos/services/libassistant/grpc/assistant_client.h
+++ b/chromeos/services/libassistant/grpc/assistant_client.h
@@ -24,6 +24,7 @@
 class OnConversationStateEventRequest;
 class OnDeviceStateEventRequest;
 class OnDisplayRequestRequest;
+class OnMediaActionFallbackEventRequest;
 class OnSpeakerIdEnrollmentEventRequest;
 class StartSpeakerIdEnrollmentRequest;
 class UpdateAssistantSettingsResponse;
@@ -78,6 +79,8 @@
   // Media:
   using MediaStatus = ::assistant::api::events::DeviceState::MediaStatus;
   using OnDeviceStateEventRequest = ::assistant::api::OnDeviceStateEventRequest;
+  using OnMediaActionFallbackEventRequest =
+      ::assistant::api::OnMediaActionFallbackEventRequest;
 
   // Conversation:
   using OnConversationStateEventRequest =
@@ -137,9 +140,10 @@
   // Sets the current media status of media playing outside of libassistant.
   // Setting external state will stop any internally playing media.
   virtual void SetExternalPlaybackState(const MediaStatus& status_proto) = 0;
-
   virtual void AddDeviceStateEventObserver(
       GrpcServicesObserver<OnDeviceStateEventRequest>* observer) = 0;
+  virtual void AddMediaActionFallbackEventObserver(
+      GrpcServicesObserver<OnMediaActionFallbackEventRequest>* observer) = 0;
 
   // Conversation methods.
   virtual void SendVoicelessInteraction(
diff --git a/chromeos/services/libassistant/grpc/assistant_client_impl.cc b/chromeos/services/libassistant/grpc/assistant_client_impl.cc
index b3c3544..d8b176d 100644
--- a/chromeos/services/libassistant/grpc/assistant_client_impl.cc
+++ b/chromeos/services/libassistant/grpc/assistant_client_impl.cc
@@ -175,6 +175,11 @@
   grpc_services_.AddDeviceStateEventObserver(observer);
 }
 
+void AssistantClientImpl::AddMediaActionFallbackEventObserver(
+    GrpcServicesObserver<OnMediaActionFallbackEventRequest>* observer) {
+  grpc_services_.AddMediaActionFallbackEventObserver(observer);
+}
+
 void AssistantClientImpl::SendVoicelessInteraction(
     const ::assistant::api::Interaction& interaction,
     const std::string& description,
diff --git a/chromeos/services/libassistant/grpc/assistant_client_impl.h b/chromeos/services/libassistant/grpc/assistant_client_impl.h
index de2ebe1..6e2793f 100644
--- a/chromeos/services/libassistant/grpc/assistant_client_impl.h
+++ b/chromeos/services/libassistant/grpc/assistant_client_impl.h
@@ -54,6 +54,9 @@
       GrpcServicesObserver<OnAssistantDisplayEventRequest>* observer) override;
   void AddDeviceStateEventObserver(
       GrpcServicesObserver<OnDeviceStateEventRequest>* observer) override;
+  void AddMediaActionFallbackEventObserver(
+      GrpcServicesObserver<OnMediaActionFallbackEventRequest>* observer)
+      override;
   void SendVoicelessInteraction(
       const ::assistant::api::Interaction& interaction,
       const std::string& description,
diff --git a/chromeos/services/libassistant/grpc/assistant_client_v1.cc b/chromeos/services/libassistant/grpc/assistant_client_v1.cc
index 533a72c..a757da8 100644
--- a/chromeos/services/libassistant/grpc/assistant_client_v1.cc
+++ b/chromeos/services/libassistant/grpc/assistant_client_v1.cc
@@ -29,6 +29,7 @@
 #include "chromeos/assistant/internal/proto/shared/proto/v2/speaker_id_enrollment_interface.pb.h"
 #include "chromeos/services/assistant/public/cpp/features.h"
 #include "chromeos/services/libassistant/callback_utils.h"
+#include "chromeos/services/libassistant/grpc/assistant_client.h"
 #include "chromeos/services/libassistant/grpc/utils/media_status_utils.h"
 #include "chromeos/services/libassistant/grpc/utils/settings_utils.h"
 #include "chromeos/services/libassistant/grpc/utils/timer_utils.h"
@@ -443,6 +444,17 @@
   device_state_event_observer_list_.AddObserver(observer);
 }
 
+void AssistantClientV1::AddMediaActionFallbackEventObserver(
+    GrpcServicesObserver<OnMediaActionFallbackEventRequest>* observer) {
+  media_action_fallback_event_observer_list_.AddObserver(observer);
+
+  // Register handler for media actions.
+  auto callback = base::BindRepeating(&AssistantClientV1::HandleMediaAction,
+                                      weak_factory_.GetWeakPtr());
+  assistant_manager_internal()->RegisterFallbackMediaHandler(
+      ToStdFunctionRepeating(BindToCurrentSequenceRepeating(callback)));
+}
+
 void AssistantClientV1::SendVoicelessInteraction(
     const ::assistant::api::Interaction& interaction,
     const std::string& description,
@@ -536,6 +548,19 @@
       media_manager_listener_.get());
 }
 
+void AssistantClientV1::HandleMediaAction(
+    const std::string& action_name,
+    const std::string& media_action_args_proto) {
+  OnMediaActionFallbackEventRequest request;
+  auto* media_action = request.mutable_event()->mutable_on_media_action_event();
+  media_action->set_action_name(action_name);
+  media_action->set_action_args(media_action_args_proto);
+
+  for (auto& observer : media_action_fallback_event_observer_list_) {
+    observer.OnGrpcMessage(request);
+  }
+}
+
 void AssistantClientV1::NotifyConversationStateEvent(
     const OnConversationStateEventRequest& request) {
   for (auto& observer : conversation_state_event_observer_list_) {
diff --git a/chromeos/services/libassistant/grpc/assistant_client_v1.h b/chromeos/services/libassistant/grpc/assistant_client_v1.h
index 693ea5b9..e736ff1c 100644
--- a/chromeos/services/libassistant/grpc/assistant_client_v1.h
+++ b/chromeos/services/libassistant/grpc/assistant_client_v1.h
@@ -57,6 +57,9 @@
   void SetExternalPlaybackState(const MediaStatus& status_proto) override;
   void AddDeviceStateEventObserver(
       GrpcServicesObserver<OnDeviceStateEventRequest>* observer) override;
+  void AddMediaActionFallbackEventObserver(
+      GrpcServicesObserver<OnMediaActionFallbackEventRequest>* observer)
+      override;
   void SendVoicelessInteraction(
       const ::assistant::api::Interaction& interaction,
       const std::string& description,
@@ -108,6 +111,8 @@
   class AssistantManagerDelegateImpl;
 
   void AddMediaManagerListener();
+  void HandleMediaAction(const std::string& action_name,
+                         const std::string& media_action_args_proto);
 
   void NotifyConversationStateEvent(
       const OnConversationStateEventRequest& request);
@@ -141,6 +146,9 @@
   base::ObserverList<GrpcServicesObserver<OnDeviceStateEventRequest>>
       device_state_event_observer_list_;
 
+  base::ObserverList<GrpcServicesObserver<OnMediaActionFallbackEventRequest>>
+      media_action_fallback_event_observer_list_;
+
   base::ObserverList<
       GrpcServicesObserver<::assistant::api::OnAlarmTimerEventRequest>>
       timer_event_observer_list_;
diff --git a/chromeos/services/libassistant/grpc/external_services/event_handler_driver.cc b/chromeos/services/libassistant/grpc/external_services/event_handler_driver.cc
index efabe8d6..1b604c0 100644
--- a/chromeos/services/libassistant/grpc/external_services/event_handler_driver.cc
+++ b/chromeos/services/libassistant/grpc/external_services/event_handler_driver.cc
@@ -16,6 +16,7 @@
 constexpr char kAssistantDisplayEventName[] = "AssistantDisplayEvent";
 constexpr char kConversationStateEventName[] = "ConversationStateEvent";
 constexpr char kDeviceStateEventName[] = "DeviceStateEvent";
+constexpr char kMediaActionFallbackEventName[] = "MediaActionFallbackEvent";
 constexpr char kHandlerMethodName[] = "OnEventFromLibas";
 
 template <typename EventSelection>
@@ -75,5 +76,16 @@
   return request;
 }
 
+template <>
+::assistant::api::RegisterEventHandlerRequest CreateRegistrationRequest<
+    ::assistant::api::MediaActionFallbackEventHandlerInterface>(
+    const std::string& assistant_service_address) {
+  ::assistant::api::RegisterEventHandlerRequest request;
+  PopulateRequest(assistant_service_address, kMediaActionFallbackEventName,
+                  &request,
+                  request.mutable_media_action_fallback_events_to_handle());
+  return request;
+}
+
 }  // namespace libassistant
 }  // namespace chromeos
diff --git a/chromeos/services/libassistant/grpc/external_services/grpc_services_initializer.cc b/chromeos/services/libassistant/grpc/external_services/grpc_services_initializer.cc
index 2ae8909..028c4bb 100644
--- a/chromeos/services/libassistant/grpc/external_services/grpc_services_initializer.cc
+++ b/chromeos/services/libassistant/grpc/external_services/grpc_services_initializer.cc
@@ -98,6 +98,12 @@
   device_state_event_handler_driver_->AddObserver(observer);
 }
 
+void GrpcServicesInitializer::AddMediaActionFallbackEventObserver(
+    GrpcServicesObserver<::assistant::api::OnMediaActionFallbackEventRequest>*
+        observer) {
+  media_action_fallback_event_handler_driver_->AddObserver(observer);
+}
+
 ActionService* GrpcServicesInitializer::GetActionService() {
   return action_handler_driver_.get();
 }
@@ -140,6 +146,14 @@
       EventHandlerDriver<::assistant::api::DeviceStateEventHandlerInterface>>(
       &server_builder_, libassistant_client_.get(), assistant_service_address_);
   service_drivers_.emplace_back(device_state_event_handler_driver_.get());
+
+  media_action_fallback_event_handler_driver_ =
+      std::make_unique<EventHandlerDriver<
+          ::assistant::api::MediaActionFallbackEventHandlerInterface>>(
+          &server_builder_, libassistant_client_.get(),
+          assistant_service_address_);
+  service_drivers_.emplace_back(
+      media_action_fallback_event_handler_driver_.get());
 }
 
 void GrpcServicesInitializer::InitLibassistGrpcClient() {
@@ -172,6 +186,7 @@
   assistant_display_event_handler_driver_->StartRegistration();
   conversation_state_event_handler_driver_->StartRegistration();
   device_state_event_handler_driver_->StartRegistration();
+  media_action_fallback_event_handler_driver_->StartRegistration();
 }
 
 }  // namespace libassistant
diff --git a/chromeos/services/libassistant/grpc/external_services/grpc_services_initializer.h b/chromeos/services/libassistant/grpc/external_services/grpc_services_initializer.h
index 1f8d7f1..2a5d2fd 100644
--- a/chromeos/services/libassistant/grpc/external_services/grpc_services_initializer.h
+++ b/chromeos/services/libassistant/grpc/external_services/grpc_services_initializer.h
@@ -24,6 +24,7 @@
 class AssistantDisplayEventHandlerInterface;
 class ConversationStateEventHandlerInterface;
 class DeviceStateEventHandlerInterface;
+class MediaActionFallbackEventHandlerInterface;
 }  // namespace api
 }  // namespace assistant
 
@@ -63,6 +64,9 @@
   void AddDeviceStateEventObserver(
       GrpcServicesObserver<::assistant::api::OnDeviceStateEventRequest>*
           observer);
+  void AddMediaActionFallbackEventObserver(
+      GrpcServicesObserver<::assistant::api::OnMediaActionFallbackEventRequest>*
+          observer);
 
   ActionService* GetActionService();
 
@@ -133,6 +137,10 @@
   std::unique_ptr<
       EventHandlerDriver<::assistant::api::DeviceStateEventHandlerInterface>>
       device_state_event_handler_driver_;
+
+  std::unique_ptr<EventHandlerDriver<
+      ::assistant::api::MediaActionFallbackEventHandlerInterface>>
+      media_action_fallback_event_handler_driver_;
 };
 
 }  // namespace libassistant
diff --git a/chromeos/services/libassistant/media_controller.cc b/chromeos/services/libassistant/media_controller.cc
index 5d00f6c..6de0ede 100644
--- a/chromeos/services/libassistant/media_controller.cc
+++ b/chromeos/services/libassistant/media_controller.cc
@@ -97,14 +97,15 @@
 
 }  // namespace
 
-class MediaController::DeviceStateEventObserver
-    : public GrpcServicesObserver<::assistant::api::OnDeviceStateEventRequest> {
+class MediaController::GrpcEventsObserver
+    : public GrpcServicesObserver<::assistant::api::OnDeviceStateEventRequest>,
+      public GrpcServicesObserver<
+          ::assistant::api::OnMediaActionFallbackEventRequest> {
  public:
-  explicit DeviceStateEventObserver(MediaController* parent)
-      : parent_(parent) {}
-  DeviceStateEventObserver(const DeviceStateEventObserver&) = delete;
-  DeviceStateEventObserver& operator=(const DeviceStateEventObserver&) = delete;
-  ~DeviceStateEventObserver() override = default;
+  explicit GrpcEventsObserver(MediaController* parent) : parent_(parent) {}
+  GrpcEventsObserver(const GrpcEventsObserver&) = delete;
+  GrpcEventsObserver& operator=(const GrpcEventsObserver&) = delete;
+  ~GrpcEventsObserver() override = default;
 
   // GrpcServicesObserver:
   // Invoked when a device state event has been received.
@@ -123,37 +124,20 @@
         ConvertMediaStatusToMojomFromV2(new_state.media_status()));
   }
 
- private:
-  mojom::MediaDelegate& delegate() { return *parent_->delegate_; }
+  // Invoked when a media action fallack event has been received.
+  void OnGrpcMessage(const ::assistant::api::OnMediaActionFallbackEventRequest&
+                         request) override {
+    if (!request.event().has_on_media_action_event())
+      return;
 
-  MediaController* const parent_;
-};
-
-class MediaController::LibassistantMediaHandler {
- public:
-  LibassistantMediaHandler(
-      MediaController* parent,
-      assistant_client::AssistantManagerInternal* assistant_manager_internal)
-      : parent_(parent),
-        mojom_task_runner_(base::SequencedTaskRunnerHandle::Get()) {
-#if !BUILDFLAG(IS_PREBUILT_LIBASSISTANT)
-    // Register handler for media actions.
-    assistant_manager_internal->RegisterFallbackMediaHandler(
-        [this](std::string action_name, std::string media_action_args_proto) {
-          HandleMediaAction(action_name, media_action_args_proto);
-        });
-#endif  // !BUILDFLAG(IS_PREBUILT_LIBASSISTANT)
+    auto media_action_event = request.event().on_media_action_event();
+    HandleMediaAction(media_action_event.action_name(),
+                      media_action_event.action_args());
   }
-  LibassistantMediaHandler(const LibassistantMediaHandler&) = delete;
-  LibassistantMediaHandler& operator=(const LibassistantMediaHandler&) = delete;
-  ~LibassistantMediaHandler() = default;
 
  private:
-  // Called from the Libassistant thread.
   void HandleMediaAction(const std::string& action_name,
                          const std::string& media_action_args_proto) {
-    ENSURE_MOJOM_THREAD(&LibassistantMediaHandler::HandleMediaAction,
-                        action_name, media_action_args_proto);
     if (action_name == kPlayMediaClientOp)
       OnPlayMedia(media_action_args_proto);
     else
@@ -229,13 +213,10 @@
   mojom::MediaDelegate& delegate() { return *parent_->delegate_; }
 
   MediaController* const parent_;
-  scoped_refptr<base::SequencedTaskRunner> mojom_task_runner_;
-  base::WeakPtrFactory<LibassistantMediaHandler> weak_factory_{this};
 };
 
 MediaController::MediaController()
-    : device_state_event_observer_(
-          std::make_unique<DeviceStateEventObserver>(this)) {}
+    : events_observer_(std::make_unique<GrpcEventsObserver>(this)) {}
 
 MediaController::~MediaController() = default;
 
@@ -271,24 +252,9 @@
 void MediaController::OnAssistantClientRunning(
     AssistantClient* assistant_client) {
   assistant_client_ = assistant_client;
-
-  handler_ = std::make_unique<LibassistantMediaHandler>(
-      this, assistant_client->assistant_manager_internal());
-
-  // |device_state_event_observer_| outlives |assistant_client_|.
-  assistant_client->AddDeviceStateEventObserver(
-      device_state_event_observer_.get());
-}
-
-void MediaController::OnDestroyingAssistantClient(
-    AssistantClient* assistant_client) {
-  assistant_client_ = nullptr;
-}
-
-void MediaController::OnAssistantClientDestroyed() {
-  // Handler can only be unset after the |AssistantManagerInternal| has been
-  // destroyed, as |AssistantManagerInternal| will call the handler.
-  handler_ = nullptr;
+  // `events_observer_` outlives `assistant_client_`.
+  assistant_client->AddDeviceStateEventObserver(events_observer_.get());
+  assistant_client->AddMediaActionFallbackEventObserver(events_observer_.get());
 }
 
 }  // namespace libassistant
diff --git a/chromeos/services/libassistant/media_controller.h b/chromeos/services/libassistant/media_controller.h
index 68c2345..93e4140 100644
--- a/chromeos/services/libassistant/media_controller.h
+++ b/chromeos/services/libassistant/media_controller.h
@@ -32,19 +32,15 @@
 
   // AssistantClientObserver implementation:
   void OnAssistantClientRunning(AssistantClient* assistant_client) override;
-  void OnDestroyingAssistantClient(AssistantClient* assistant_client) override;
-  void OnAssistantClientDestroyed() override;
 
  private:
-  class DeviceStateEventObserver;
-  class LibassistantMediaHandler;
+  class GrpcEventsObserver;
 
   AssistantClient* assistant_client_ = nullptr;
 
   mojo::Receiver<mojom::MediaController> receiver_{this};
   mojo::Remote<mojom::MediaDelegate> delegate_;
-  std::unique_ptr<DeviceStateEventObserver> device_state_event_observer_;
-  std::unique_ptr<LibassistantMediaHandler> handler_;
+  std::unique_ptr<GrpcEventsObserver> events_observer_;
 };
 
 }  // namespace libassistant
diff --git a/chromeos/services/libassistant/test_support/fake_assistant_client.cc b/chromeos/services/libassistant/test_support/fake_assistant_client.cc
index 1ee6c8c..ccf3377f7 100644
--- a/chromeos/services/libassistant/test_support/fake_assistant_client.cc
+++ b/chromeos/services/libassistant/test_support/fake_assistant_client.cc
@@ -88,6 +88,9 @@
 void FakeAssistantClient::AddConversationStateEventObserver(
     GrpcServicesObserver<OnConversationStateEventRequest>* observer) {}
 
+void FakeAssistantClient::AddMediaActionFallbackEventObserver(
+    GrpcServicesObserver<OnMediaActionFallbackEventRequest>* observer) {}
+
 void FakeAssistantClient::SetInternalOptions(const std::string& locale,
                                              bool spoken_feedback_enabled) {}
 
diff --git a/chromeos/services/libassistant/test_support/fake_assistant_client.h b/chromeos/services/libassistant/test_support/fake_assistant_client.h
index f7f4126..9bfc1af 100644
--- a/chromeos/services/libassistant/test_support/fake_assistant_client.h
+++ b/chromeos/services/libassistant/test_support/fake_assistant_client.h
@@ -56,6 +56,9 @@
   void SetExternalPlaybackState(const MediaStatus& status_proto) override;
   void AddDeviceStateEventObserver(
       GrpcServicesObserver<OnDeviceStateEventRequest>* observer) override;
+  void AddMediaActionFallbackEventObserver(
+      GrpcServicesObserver<OnMediaActionFallbackEventRequest>* observer)
+      override;
   void SendVoicelessInteraction(
       const ::assistant::api::Interaction& interaction,
       const std::string& description,
diff --git a/components/autofill/android/BUILD.gn b/components/autofill/android/BUILD.gn
index d2902f2..7e080415 100644
--- a/components/autofill/android/BUILD.gn
+++ b/components/autofill/android/BUILD.gn
@@ -48,7 +48,6 @@
     "payments/java/res/drawable-xxxhdpi/unionpay_card.png",
     "payments/java/res/drawable/discover_card.xml",
     "payments/java/res/drawable/elo_card.xml",
-    "payments/java/res/drawable/google_pay_plex.xml",
     "payments/java/res/drawable/ic_credit_card_black.xml",
     "payments/java/res/drawable/mir_card.xml",
     "payments/java/res/drawable/visa_card.xml",
diff --git a/components/autofill/android/payments/java/res/drawable/google_pay_plex.xml b/components/autofill/android/payments/java/res/drawable/google_pay_plex.xml
deleted file mode 100644
index ce75376..0000000
--- a/components/autofill/android/payments/java/res/drawable/google_pay_plex.xml
+++ /dev/null
@@ -1,34 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright 2021 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. -->
-
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
-    android:width="32dp"
-    android:height="20dp"
-    android:viewportWidth="32"
-    android:viewportHeight="20">
-  <path
-      android:pathData="M2,0L30,0A2,2 0,0 1,32 2L32,18A2,2 0,0 1,30 20L2,20A2,2 0,0 1,0 18L0,2A2,2 0,0 1,2 0z"
-      android:fillColor="#ffffff"/>
-  <path
-      android:pathData="M18.8265,7.1284L19.7068,7.6356C20.409,8.0397 20.6499,8.9378 20.244,9.6391L17.9584,13.5869L18.9118,14.1363C19.6149,14.5413 20.5131,14.3003 20.9199,13.5981L23.1371,9.7684C23.8806,8.484 23.4418,6.8434 22.1584,6.0981C21.0212,5.4503 19.5728,5.8403 18.9174,6.9719L18.8265,7.1284Z"
-      android:strokeWidth="1.5"
-      android:fillColor="#00000000"
-      android:strokeColor="#EA4335"/>
-  <path
-      android:pathData="M19.7097,7.6364L19.1519,7.3148C18.9728,7.2117 18.7441,7.2726 18.64,7.4517L15.746,12.4504C15.28,13.2558 14.2506,13.5314 13.4444,13.0673L12.6813,12.6276C12.011,13.8042 12.3663,15.2883 13.4781,16.0355C14.7625,16.748 16.3872,16.3036 17.125,15.0295L20.2469,9.637C20.651,8.9367 20.411,8.0404 19.7097,7.6364Z"
-      android:strokeWidth="1.5"
-      android:fillColor="#00000000"
-      android:strokeColor="#FBBC04"/>
-  <path
-      android:pathData="M12.864,12.7346L13.4415,13.0674C14.2487,13.5324 15.28,13.2558 15.7468,12.4496L18.9184,6.9718C19.5756,5.8365 21.0306,5.4474 22.1687,6.1027L20.1081,4.9159L18.6493,4.0759C17.0359,3.1468 14.9734,3.6981 14.0415,5.3068L13.5437,6.1665L14.4353,6.6783C15.1384,7.0824 15.3793,7.9758 14.9734,8.6752L12.7862,12.4439C12.7281,12.5461 12.7628,12.6755 12.864,12.7346Z"
-      android:strokeWidth="1.5"
-      android:fillColor="#00000000"
-      android:strokeColor="#34A853"/>
-  <path
-      android:pathData="M14.4362,6.6793L12.8594,5.7736C12.1562,5.3696 11.2581,5.6096 10.8522,6.3089L8.9603,9.5686C8.0294,11.1736 8.5816,13.2258 10.195,14.1521L11.3959,14.8421L12.8519,15.6783L13.4837,16.0402C12.3616,15.2911 12.0062,13.7911 12.6916,12.6099L13.1809,11.7661L14.9744,8.6752C15.3794,7.9768 15.1394,7.0824 14.4362,6.6793Z"
-      android:strokeWidth="1.5"
-      android:fillColor="#00000000"
-      android:strokeColor="#4285F4"/>
-</vector>
diff --git a/components/autofill/content/browser/form_forest.cc b/components/autofill/content/browser/form_forest.cc
index 2b7d117..45f6d97 100644
--- a/components/autofill/content/browser/form_forest.cc
+++ b/components/autofill/content/browser/form_forest.cc
@@ -15,6 +15,7 @@
 #include "base/stl_util.h"
 #include "components/autofill/content/browser/form_forest_util_inl.h"
 #include "components/autofill/core/common/autofill_constants.h"
+#include "components/autofill/core/common/autofill_features.h"
 #include "content/public/browser/render_process_host.h"
 #include "third_party/abseil-cpp/absl/types/variant.h"
 #include "third_party/blink/public/common/permissions_policy/permissions_policy_features.h"
@@ -618,8 +619,8 @@
             return true;
         }
       };
-      // Fields in frames whose permissions policy allows shared-autofill may
-      // be filled if the |triggered_origin| is the main origin.
+      // Fields whose document enables the policy-controlled feature
+      // shared-autofill may be safe to fill.
       auto HasSharedAutofillPermission = [&mutable_this](
                                              LocalFrameToken frame_token) {
         FrameData* frame = mutable_this.GetFrameData(frame_token);
@@ -632,12 +633,17 @@
       auto it = field_type_map.find(field.global_id());
       ServerFieldType field_type =
           it != field_type_map.end() ? it->second : UNKNOWN_TYPE;
-      return field.origin == triggered_origin ||
-             (field.origin == main_origin &&
-              HasSharedAutofillPermission(renderer_form->host_frame) &&
-              !IsSensitiveFieldType(field_type)) ||
-             (triggered_origin == main_origin &&
-              HasSharedAutofillPermission(renderer_form->host_frame));
+      if (features::kAutofillSharedAutofillRelaxedParam.Get()) {
+        return field.origin == triggered_origin ||
+               HasSharedAutofillPermission(renderer_form->host_frame);
+      } else {
+        return field.origin == triggered_origin ||
+               (field.origin == main_origin &&
+                HasSharedAutofillPermission(renderer_form->host_frame) &&
+                !IsSensitiveFieldType(field_type)) ||
+               (triggered_origin == main_origin &&
+                HasSharedAutofillPermission(renderer_form->host_frame));
+      }
     };
 
     renderer_form->fields.push_back(browser_field);
diff --git a/components/autofill/content/browser/form_forest.h b/components/autofill/content/browser/form_forest.h
index 6d74370c..2fbabde 100644
--- a/components/autofill/content/browser/form_forest.h
+++ b/components/autofill/content/browser/form_forest.h
@@ -232,25 +232,36 @@
   // The |field_type_map| should contain the field types of the fields in
   // |browser_form|.
   //
-  // A field is *safe to fill* iff at least one of the conditions (1), (2), (3)
-  // and additionally condition (4) hold:
+  // There are two modes that determine whether a field is *safe to fill*.
+  // By default, a field is safe to fill iff at least one of the conditions
+  // (1–3) and additionally condition (4) hold:
+  //
   // (1) The field's origin is the |triggered_origin|.
-  // (2) The field's origin is the main origin and the field's type in
-  //     |field_type_map| is not sensitive (see is_sensitive_field_type()).
-  // (3) The |triggered_origin| is main origin and the field's frame's
-  //     permissions policy allows shared-autofill.
+  // (2) The field's origin is the main origin, the field's type in
+  //     |field_type_map| is not sensitive (see IsSensitiveFieldType()), and the
+  //     policy-controlled feature shared-autofill is enabled in the field's
+  //     frame.
+  // (3) The |triggered_origin| is the main origin and the policy-controlled
+  //     feature shared-autofill is enabled in the field's frame.
   // (4) No frame on the shortest path from the field on which Autofill was
   //     triggered to the field in question, except perhaps the shallowest
   //     frame, is a fenced frame.
   //
+  // If the Finch parameter relax_shared_autofill is true, the restriction to
+  // the main origin in condition 3 is lifted. Thus, conditions (2) and (3)
+  // reduce to the following:
+  //
+  // (2+3) The policy-controlled feature shared-autofill is enabled in the
+  //       field's document.
+  //
   // The *origin of a field* is the origin of the frame that contains the
   // corresponding form-control element.
   //
   // The *main origin* is `browser_form.main_frame_origin`.
   //
-  // A frame's *permissions policy allows shared-autofill* if that frame is a
-  // main frame or its embedding <iframe> element lists "shared-autofill" in
-  // its "allow" attribute (see https://www.w3.org/TR/permissions-policy-1/).
+  // The "allow" attribute of the <iframe> element controls whether the
+  // *policy-controlled feature shared-autofill* is enabled in a document
+  // (see https://www.w3.org/TR/permissions-policy-1/).
   RendererForms GetRendererFormsOfBrowserForm(
       const FormData& browser_form,
       const url::Origin& triggered_origin,
diff --git a/components/autofill/content/browser/form_forest_unittest.cc b/components/autofill/content/browser/form_forest_unittest.cc
index b83395c9..6031aaf5 100644
--- a/components/autofill/content/browser/form_forest_unittest.cc
+++ b/components/autofill/content/browser/form_forest_unittest.cc
@@ -12,10 +12,12 @@
 
 #include "base/strings/strcat.h"
 #include "base/strings/string_piece.h"
+#include "base/test/scoped_feature_list.h"
 #include "components/autofill/content/browser/form_forest.h"
 #include "components/autofill/content/browser/form_forest_test_api.h"
 #include "components/autofill/content/browser/form_forest_util_inl.h"
 #include "components/autofill/core/browser/autofill_test_utils.h"
+#include "components/autofill/core/common/autofill_features.h"
 #include "content/public/test/navigation_simulator.h"
 #include "content/public/test/test_renderer_host.h"
 #include "testing/gmock/include/gmock/gmock.h"
@@ -312,6 +314,13 @@
   // FormForest::GetBrowserFormOfRendererForm() for details).
   enum class Policy { kDefault, kSharedAutofill, kNoSharedAutofill };
 
+  explicit FormForestTest(bool relax_shared_autofill = false) {
+    feature_list_.InitAndEnableFeatureWithParameters(
+        features::kAutofillSharedAutofill,
+        {{features::kAutofillSharedAutofillRelaxedParam.name,
+          relax_shared_autofill ? "true" : "false"}});
+  }
+
   void SetUp() override {
     RenderViewHostTestHarness::SetUp();
     CHECK(kOpaqueOrigin.opaque());
@@ -400,6 +409,7 @@
     return it->second.get();
   }
 
+  base::test::ScopedFeatureList feature_list_;
   std::map<content::RenderFrameHost*,
            std::unique_ptr<MockContentAutofillDriver>>
       autofill_drivers_;
@@ -437,6 +447,10 @@
     size_t count = base::dynamic_extent;
   };
 
+  explicit FormForestTestWithMockedTree(bool relax_shared_autofill = false)
+      : FormForestTest(
+            /*relax_shared_autofill=*/relax_shared_autofill) {}
+
   void TearDown() override {
     mocked_forms_.Reset();
     flattened_forms_.Reset();
@@ -1399,6 +1413,11 @@
 // Tests of FormForest::GetRendererFormsOfBrowserForm().
 
 class FormForestTestUnflatten : public FormForestTestWithMockedTree {
+ public:
+  explicit FormForestTestUnflatten(bool relax_shared_autofill = false)
+      : FormForestTestWithMockedTree(
+            /*relax_shared_autofill=*/relax_shared_autofill) {}
+
  protected:
   // The subject of this test fixture.
   std::vector<FormData> GetRendererFormsOfBrowserForm(
@@ -1570,9 +1589,17 @@
 }
 
 // Fixture for the shared-autofill policy tests.
+// The parameter controls the value of relax_shared_autofill.
 class FormForestTestUnflattenSharedAutofillPolicy
-    : public FormForestTestUnflatten {
+    : public FormForestTestUnflatten,
+      public ::testing::WithParamInterface<bool> {
  public:
+  FormForestTestUnflattenSharedAutofillPolicy()
+      : FormForestTestUnflatten(
+            /*relax_shared_autofill=*/relax_shared_autofill()) {}
+
+  bool relax_shared_autofill() const { return GetParam(); }
+
   void SetUp() override {
     FormForestTestUnflatten::SetUp();
     MockFormForest(
@@ -1589,7 +1616,7 @@
 };
 
 // Tests filling into frames with shared-autofill policy from the main origin.
-TEST_F(FormForestTestUnflattenSharedAutofillPolicy, FromMainOrigin) {
+TEST_P(FormForestTestUnflattenSharedAutofillPolicy, FromMainOrigin) {
   MockFlattening({{"main"}, {"disallowed"}, {"allowed"}});
   std::vector<FormData> expectation = {
       WithValues(GetMockedForm("main"), Profile(0)),
@@ -1600,12 +1627,18 @@
 }
 
 // Tests filling into frames with shared-autofill policy from the main origin.
-TEST_F(FormForestTestUnflattenSharedAutofillPolicy, FromOtherOrigin) {
+TEST_P(FormForestTestUnflattenSharedAutofillPolicy, FromOtherOrigin) {
   MockFlattening({{"main"}, {"disallowed"}, {"allowed"}});
-  std::vector<FormData> expectation = {
-      WithoutValues(GetMockedForm("main")),
-      WithValues(GetMockedForm("disallowed"), Profile(1)),
-      WithoutValues(GetMockedForm("allowed"))};
+  std::vector<FormData> expectation;
+  if (!relax_shared_autofill()) {
+    expectation = {WithoutValues(GetMockedForm("main")),
+                   WithValues(GetMockedForm("disallowed"), Profile(1)),
+                   WithoutValues(GetMockedForm("allowed"))};
+  } else {
+    expectation = {WithValues(GetMockedForm("main"), Profile(0)),
+                   WithValues(GetMockedForm("disallowed"), Profile(1)),
+                   WithValues(GetMockedForm("allowed"), Profile(2))};
+  }
   EXPECT_THAT(GetRendererFormsOfBrowserForm("main", Origin(kOtherUrl), {}),
               UnorderedArrayEquals(expectation));
 }
@@ -1680,6 +1713,10 @@
   EXPECT_EQ(num_equals_calls_, GetParam().expected_comparisons);
 }
 
+INSTANTIATE_TEST_SUITE_P(FormForestTest,
+                         FormForestTestUnflattenSharedAutofillPolicy,
+                         testing::Bool());
+
 INSTANTIATE_TEST_SUITE_P(
     FormForestTest,
     ForEachInSetDifferenceTest,
diff --git a/components/autofill/core/browser/browser_autofill_manager_unittest.cc b/components/autofill/core/browser/browser_autofill_manager_unittest.cc
index ccff80df..bdec93d 100644
--- a/components/autofill/core/browser/browser_autofill_manager_unittest.cc
+++ b/components/autofill/core/browser/browser_autofill_manager_unittest.cc
@@ -1775,121 +1775,6 @@
                  browser_autofill_manager_->GetPackedCreditCardID(5)));
 }
 
-TEST_P(BrowserAutofillManagerStructuredProfileTest,
-       GetCreditCardSuggestions_GoogleIssuedCard_CCNumber) {
-  base::test::ScopedFeatureList features;
-  features.InitAndEnableFeature(
-      autofill::features::kAutofillEnableGoogleIssuedCard);
-  personal_data_.ClearCreditCards();
-  // Add a Google Issued Card.
-  CreditCard google_issued_card;
-  test::SetCreditCardInfo(&google_issued_card, "Lorem Ispium",
-                          "5555555555554444",  // Mastercard
-                          "10", "2998", "1");
-  google_issued_card.set_guid("00000000-0000-0000-0000-000000000007");
-  google_issued_card.set_record_type(
-      CreditCard::RecordType::MASKED_SERVER_CARD);
-  google_issued_card.set_card_issuer(CreditCard::Issuer::GOOGLE);
-  personal_data_.AddServerCreditCard(google_issued_card);
-  // Set up our form data.
-  FormData form;
-  CreateTestCreditCardFormData(&form, true, false);
-  std::vector<FormData> forms(1, form);
-  FormsSeen(forms);
-  // Set the field being edited to CC field.
-  const FormFieldData& credit_card_number_field = form.fields[1];
-  const std::string google_issued_card_value = base::JoinString(
-      {"Plex Mastercard  ", test::ObfuscatedCardDigitsAsUTF8("4444")}, "");
-#if BUILDFLAG(IS_ANDROID) || BUILDFLAG(IS_IOS)
-  const std::string google_issued_card_label = std::string("10/98");
-#else
-  const std::string google_issued_card_label = std::string("Expires on 10/98");
-#endif
-
-  GetAutofillSuggestions(form, credit_card_number_field);
-
-  CheckSuggestions(
-      kDefaultPageID,
-      Suggestion(google_issued_card_value, google_issued_card_label,
-                 kGoogleIssuedCard,
-                 browser_autofill_manager_->GetPackedCreditCardID(7)));
-}
-
-TEST_P(BrowserAutofillManagerStructuredProfileTest,
-       GetCreditCardSuggestions_GoogleIssuedCard_NonCCNumber) {
-  base::test::ScopedFeatureList features;
-  features.InitAndEnableFeature(
-      autofill::features::kAutofillEnableGoogleIssuedCard);
-  personal_data_.ClearCreditCards();
-  // Add a Google Issued Card.
-  CreditCard google_issued_card;
-  test::SetCreditCardInfo(&google_issued_card, "Lorem Ispium",
-                          "5555555555554444",  // Mastercard
-                          "10", "2998", "1");
-  google_issued_card.set_guid("00000000-0000-0000-0000-000000000007");
-  google_issued_card.set_record_type(
-      CreditCard::RecordType::MASKED_SERVER_CARD);
-  google_issued_card.set_card_issuer(CreditCard::Issuer::GOOGLE);
-  personal_data_.AddServerCreditCard(google_issued_card);
-  // Set up our form data.
-  FormData form;
-  CreateTestCreditCardFormData(&form, true, false);
-  std::vector<FormData> forms(1, form);
-  FormsSeen(forms);
-  // Set the field being edited to the cardholder name field.
-  const FormFieldData& cardholder_name_field = form.fields[0];
-#if BUILDFLAG(IS_ANDROID)
-  const std::string google_issued_card_label = base::JoinString(
-      {"Plex Mastercard  ", test::ObfuscatedCardDigitsAsUTF8("4444")}, "");
-#elif BUILDFLAG(IS_IOS)
-  const std::string google_issued_card_label =
-      test::ObfuscatedCardDigitsAsUTF8("4444");
-#else
-  const std::string google_issued_card_label = base::JoinString(
-      {"Plex Mastercard  ", test::ObfuscatedCardDigitsAsUTF8("4444"),
-       ", expires on 10/98"},
-      "");
-#endif
-
-  GetAutofillSuggestions(form, cardholder_name_field);
-
-  CheckSuggestions(
-      kDefaultPageID,
-      Suggestion("Lorem Ispium", google_issued_card_label, kGoogleIssuedCard,
-                 browser_autofill_manager_->GetPackedCreditCardID(7)));
-}
-
-TEST_P(BrowserAutofillManagerStructuredProfileTest,
-       GetCreditCardSuggestions_GoogleIssuedCardNotPresent_ExpOff) {
-  base::test::ScopedFeatureList features;
-  features.InitAndDisableFeature(
-      autofill::features::kAutofillEnableGoogleIssuedCard);
-  // Add 2 Server cards.
-  CreateTestServerCreditCards();
-  // Add a Google Issued Card.
-  CreditCard google_issued_card;
-  test::SetCreditCardInfo(&google_issued_card, "Lorem Ispium",
-                          "5555555555554444",  // Mastercard
-                          "10", "2998", "1");
-  google_issued_card.set_guid("00000000-0000-0000-0000-000000000007");
-  google_issued_card.set_record_type(
-      CreditCard::RecordType::MASKED_SERVER_CARD);
-  google_issued_card.set_card_issuer(CreditCard::Issuer::GOOGLE);
-  personal_data_.AddServerCreditCard(google_issued_card);
-  // Set up our form data.
-  FormData form;
-  CreateTestCreditCardFormData(&form, true, false);
-  std::vector<FormData> forms(1, form);
-  FormsSeen(forms);
-  // Set the field being edited to CC field.
-  const FormFieldData& credit_card_number_field = form.fields[1];
-
-  GetAutofillSuggestions(form, credit_card_number_field);
-
-  // Assert that there are only two credit card suggestions returned.
-  external_delegate_->CheckSuggestionCount(kDefaultPageID, 2);
-}
-
 // Test that we will eventually return the credit card signin promo when there
 // are no credit card suggestions and the promo is active. See the tests in
 // AutofillExternalDelegateTest that test whether the promo is added.
diff --git a/components/autofill/core/browser/data_model/credit_card.cc b/components/autofill/core/browser/data_model/credit_card.cc
index 50029fb..3a3b25c 100644
--- a/components/autofill/core/browser/data_model/credit_card.cc
+++ b/components/autofill/core/browser/data_model/credit_card.cc
@@ -580,10 +580,6 @@
   base::TrimString(nickname_, u" ", &nickname_);
 }
 
-bool CreditCard::IsGoogleIssuedCard() const {
-  return card_issuer_ == CreditCard::Issuer::GOOGLE;
-}
-
 void CreditCard::operator=(const CreditCard& credit_card) {
   set_use_count(credit_card.use_count());
   set_use_date(credit_card.use_date());
@@ -908,10 +904,6 @@
 }
 
 std::string CreditCard::CardIconStringForAutofillSuggestion() const {
-  if (base::FeatureList::IsEnabled(features::kAutofillEnableGoogleIssuedCard) &&
-      IsGoogleIssuedCard()) {
-    return kGoogleIssuedCard;
-  }
   return network_;
 }
 
@@ -937,16 +929,7 @@
   if (HasNonEmptyValidNickname() || !customized_nickname.empty()) {
     return NicknameAndLastFourDigits(customized_nickname, obfuscation_length);
   }
-  std::u16string networkAndLastFourDigits =
-      NetworkAndLastFourDigits(obfuscation_length);
-  // Add Plex before the network and last four digits to identify it as a Google
-  // Plex card.
-  if (base::FeatureList::IsEnabled(features::kAutofillEnableGoogleIssuedCard) &&
-      IsGoogleIssuedCard()) {
-    return l10n_util::GetStringUTF16(IDS_AUTOFILL_CC_GOOGLE_ISSUED) + u" " +
-           networkAndLastFourDigits;
-  }
-  return networkAndLastFourDigits;
+  return NetworkAndLastFourDigits(obfuscation_length);
 }
 
 #if BUILDFLAG(IS_ANDROID)
@@ -1172,7 +1155,6 @@
 const char kDiscoverCard[] = "discoverCC";
 const char kEloCard[] = "eloCC";
 const char kGenericCard[] = "genericCC";
-const char kGoogleIssuedCard[] = "googleIssuedCC";
 const char kJCBCard[] = "jcbCC";
 const char kMasterCard[] = "masterCardCC";
 const char kMirCard[] = "mirCC";
diff --git a/components/autofill/core/browser/data_model/credit_card.h b/components/autofill/core/browser/data_model/credit_card.h
index a8a3592..a93b1ed4 100644
--- a/components/autofill/core/browser/data_model/credit_card.h
+++ b/components/autofill/core/browser/data_model/credit_card.h
@@ -460,7 +460,6 @@
 extern const char kDiscoverCard[];
 extern const char kEloCard[];
 extern const char kGenericCard[];
-extern const char kGoogleIssuedCard[];
 extern const char kJCBCard[];
 extern const char kMasterCard[];
 extern const char kMirCard[];
diff --git a/components/autofill/core/browser/data_model/credit_card_unittest.cc b/components/autofill/core/browser/data_model/credit_card_unittest.cc
index 580a8bb..40ad9c2 100644
--- a/components/autofill/core/browser/data_model/credit_card_unittest.cc
+++ b/components/autofill/core/browser/data_model/credit_card_unittest.cc
@@ -256,119 +256,6 @@
       credit_card2.CardIdentifierStringForAutofillDisplay());
 }
 
-TEST(CreditCardTest, CardIdentifierStringForIssuedCard) {
-  base::test::ScopedFeatureList scoped_feature_list;
-  // Enable the flag.
-  scoped_feature_list.InitAndEnableFeature(
-      features::kAutofillEnableGoogleIssuedCard);
-  // Case 1: Card Issuer set to GOOGLE with no nickname.
-  CreditCard credit_card1(base::GenerateGUID(), "https://www.example.com/");
-  credit_card1.set_card_issuer(CreditCard::Issuer::GOOGLE);
-  test::SetCreditCardInfo(&credit_card1, "John Dillinger",
-                          "5105 1051 0510 5100" /* Mastercard */, "01", "2020",
-                          "1");
-  EXPECT_EQ(l10n_util::GetStringUTF16(IDS_AUTOFILL_CC_GOOGLE_ISSUED) +
-                UTF8ToUTF16(std::string(" Mastercard  ") +
-                            test::ObfuscatedCardDigitsAsUTF8("5100")),
-            credit_card1.CardIdentifierStringForAutofillDisplay());
-
-  // Case 2: Card Issuer set to GOOGLE with nickname.
-  std::u16string valid_nickname = u"My Visa Card";
-  credit_card1.SetNickname(valid_nickname);
-  EXPECT_EQ(
-      valid_nickname + UTF8ToUTF16(std::string("  ") +
-                                   test::ObfuscatedCardDigitsAsUTF8("5100")),
-      credit_card1.CardIdentifierStringForAutofillDisplay());
-
-  // Case 3: Card Issuer set to ISSUER_UNKNOWN and no nickname.
-  CreditCard credit_card2(base::GenerateGUID(), "https://www.example.com/");
-  test::SetCreditCardInfo(&credit_card2, "John Dillinger",
-                          "5105 1051 0510 5100" /* Mastercard */, "01", "2020",
-                          "1");
-  credit_card2.set_card_issuer(CreditCard::Issuer::ISSUER_UNKNOWN);
-  EXPECT_EQ(UTF8ToUTF16(std::string("Mastercard  ") +
-                        test::ObfuscatedCardDigitsAsUTF8("5100")),
-            credit_card2.CardIdentifierStringForAutofillDisplay());
-}
-
-TEST(CreditCardTest, CardIdentifierStringForIssuedCardExpOff) {
-  base::test::ScopedFeatureList scoped_feature_list;
-  // Disable the flag.
-  scoped_feature_list.InitAndDisableFeature(
-      features::kAutofillEnableGoogleIssuedCard);
-  // Case 1: Card Issuer set to GOOGLE with no nickname.
-  CreditCard credit_card1(base::GenerateGUID(), "https://www.example.com/");
-  credit_card1.set_card_issuer(CreditCard::Issuer::GOOGLE);
-  test::SetCreditCardInfo(&credit_card1, "John Dillinger",
-                          "5105 1051 0510 5100" /* Mastercard */, "01", "2020",
-                          "1");
-  EXPECT_EQ(UTF8ToUTF16(std::string("Mastercard  ") +
-                        test::ObfuscatedCardDigitsAsUTF8("5100")),
-            credit_card1.CardIdentifierStringForAutofillDisplay());
-
-  // Case 2: Card Issuer set to GOOGLE with nickname.
-  std::u16string valid_nickname = u"My Visa Card";
-  credit_card1.SetNickname(valid_nickname);
-  EXPECT_EQ(
-      valid_nickname + UTF8ToUTF16(std::string("  ") +
-                                   test::ObfuscatedCardDigitsAsUTF8("5100")),
-      credit_card1.CardIdentifierStringForAutofillDisplay());
-
-  // Case 3: Card Issuer set to ISSUER_UNKNOWN and no nickname.
-  CreditCard credit_card2(base::GenerateGUID(), "https://www.example.com/");
-  test::SetCreditCardInfo(&credit_card2, "John Dillinger",
-                          "5105 1051 0510 5100" /* Mastercard */, "01", "2020",
-                          "1");
-  credit_card2.set_card_issuer(CreditCard::Issuer::ISSUER_UNKNOWN);
-  EXPECT_EQ(UTF8ToUTF16(std::string("Mastercard  ") +
-                        test::ObfuscatedCardDigitsAsUTF8("5100")),
-            credit_card2.CardIdentifierStringForAutofillDisplay());
-}
-
-TEST(CreditCardTest, CardIconStringForGoogleIssuedCard) {
-  base::test::ScopedFeatureList scoped_feature_list;
-  // Enable the flag.
-  scoped_feature_list.InitAndEnableFeature(
-      features::kAutofillEnableGoogleIssuedCard);
-  // Case 1: Card Issuer set to GOOGLE.
-  CreditCard credit_card1(base::GenerateGUID(), "https://www.example.com/");
-  credit_card1.set_card_issuer(CreditCard::Issuer::GOOGLE);
-  test::SetCreditCardInfo(&credit_card1, "John Dillinger", "", "01", "2020",
-                          "1");
-  EXPECT_EQ(kGoogleIssuedCard,
-            credit_card1.CardIconStringForAutofillSuggestion());
-
-  // Case 2: Card Issuer set to ISSUER_UNKNOWN.
-  CreditCard credit_card2(base::GenerateGUID(), "https://www.example.com/");
-  test::SetCreditCardInfo(&credit_card2, "John Dillinger",
-                          "5105 1051 0510 5100" /* Mastercard */, "01", "2020",
-                          "1");
-  credit_card2.set_card_issuer(CreditCard::Issuer::ISSUER_UNKNOWN);
-  EXPECT_EQ(kMasterCard, credit_card2.CardIconStringForAutofillSuggestion());
-}
-
-TEST(CreditCardTest, CardIconStringForGoogleIssuedCardExpOff) {
-  base::test::ScopedFeatureList scoped_feature_list;
-  // Enable the flag.
-  scoped_feature_list.InitAndDisableFeature(
-      features::kAutofillEnableGoogleIssuedCard);
-  // Case 1: Card Issuer set to GOOGLE.
-  CreditCard credit_card1(base::GenerateGUID(), "https://www.example.com/");
-  credit_card1.set_card_issuer(CreditCard::Issuer::GOOGLE);
-  test::SetCreditCardInfo(&credit_card1, "John Dillinger",
-                          "5105 1051 0510 5100" /* Mastercard */, "01", "2020",
-                          "1");
-  EXPECT_EQ(kMasterCard, credit_card1.CardIconStringForAutofillSuggestion());
-
-  // Case 2: Card Issuer set to ISSUER_UNKNOWN.
-  CreditCard credit_card2(base::GenerateGUID(), "https://www.example.com/");
-  test::SetCreditCardInfo(&credit_card2, "John Dillinger",
-                          "5105 1051 0510 5100" /* Mastercard */, "01", "2020",
-                          "1");
-  credit_card2.set_card_issuer(CreditCard::Issuer::ISSUER_UNKNOWN);
-  EXPECT_EQ(kMasterCard, credit_card2.CardIconStringForAutofillSuggestion());
-}
-
 TEST(CreditCardTest, AssignmentOperator) {
   CreditCard a(base::GenerateGUID(), test::kEmptyOrigin);
   test::SetCreditCardInfo(&a, "John Dillinger", "123456789012", "01", "2010",
diff --git a/components/autofill/core/browser/payments/credit_card_access_manager.cc b/components/autofill/core/browser/payments/credit_card_access_manager.cc
index 699090c..b1661c2 100644
--- a/components/autofill/core/browser/payments/credit_card_access_manager.cc
+++ b/components/autofill/core/browser/payments/credit_card_access_manager.cc
@@ -616,21 +616,6 @@
         unmask_auth_flow_type_);
   }
 
-  // Local boolean denotes whether to show the dialog that offers opting-in to
-  // FIDO authentication after the CVC check. Note that this and
-  // |should_respond_immediately| are NOT mutually exclusive. If both are true,
-  // it represents the Desktop opt-in flow (fill the form first, and prompt the
-  // opt-in dialog).
-  bool should_offer_fido_auth = false;
-  // For iOS, FIDO auth is not supported yet. For Android, users have already
-  // been offered opt-in at this point.
-#if !BUILDFLAG(IS_IOS) && !BUILDFLAG(IS_ANDROID)
-  should_offer_fido_auth = unmask_details_.offer_fido_opt_in &&
-                           !response.card_authorization_token.empty() &&
-                           !GetOrCreateFIDOAuthenticator()
-                                ->GetOrCreateFidoAuthenticationStrikeDatabase()
-                                ->IsMaxStrikesLimitReached();
-#endif
   bool should_register_card_with_fido = ShouldRegisterCardWithFido(response);
   if (ShouldRespondImmediately(response)) {
     // If ShouldRespondImmediately() returns true,
@@ -665,7 +650,7 @@
                                               request_options->Clone());
 #endif
   }
-  if (should_offer_fido_auth) {
+  if (ShouldOfferFidoOptInDialog(response)) {
     // CreditCardFIDOAuthenticator will handle enrollment completely.
     ShowWebauthnOfferDialog(response.card_authorization_token);
   }
@@ -673,9 +658,9 @@
   HandleFidoOptInStatusChange();
   // TODO(crbug.com/1249665): Add Reset() to this function after cleaning up the
   // FIDO opt-in status change. This should not have any negative impact now
-  // except for readability and cleanness. |should_offer_fido_auth| and
-  // |opt_in_intention_| are to some extent duplicate. We should be able to
-  // remove the two variables and use a function.
+  // except for readability and cleanness. The result of
+  // ShouldOfferFidoOptInDialog() and |opt_in_intention_| are to some extent
+  // duplicate. We should be able to combine them into one function.
 }
 
 #if BUILDFLAG(IS_ANDROID)
@@ -895,6 +880,36 @@
   return false;
 }
 
+bool CreditCardAccessManager::ShouldOfferFidoOptInDialog(
+    const CreditCardCVCAuthenticator::CVCAuthenticationResponse& response) {
+#if BUILDFLAG(IS_IOS) || BUILDFLAG(IS_ANDROID)
+  // We should not offer FIDO opt-in dialog on mobile.
+  return false;
+#else
+  // If this card is not eligible for offering FIDO opt-in, we should not offer
+  // the FIDO opt-in dialog.
+  if (!unmask_details_.offer_fido_opt_in)
+    return false;
+
+  // A card authorization token is required for FIDO opt-in, so if we did not
+  // receive one from the server we should not offer the FIDO opt-in dialog.
+  if (response.card_authorization_token.empty())
+    return false;
+
+  // If the strike limit was reached for the FIDO opt-in dialog, we should not
+  // offer it.
+  if (GetOrCreateFIDOAuthenticator()
+          ->GetOrCreateFidoAuthenticationStrikeDatabase()
+          ->IsMaxStrikesLimitReached()) {
+    return false;
+  }
+
+  // None of the cases where we should not offer the FIDO opt-in dialog were
+  // true, so we should offer it.
+  return true;
+#endif
+}
+
 void CreditCardAccessManager::ShowWebauthnOfferDialog(
     std::string card_authorization_token) {
 #if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS)
diff --git a/components/autofill/core/browser/payments/credit_card_access_manager.h b/components/autofill/core/browser/payments/credit_card_access_manager.h
index 55f0251..6b29217 100644
--- a/components/autofill/core/browser/payments/credit_card_access_manager.h
+++ b/components/autofill/core/browser/payments/credit_card_access_manager.h
@@ -323,6 +323,13 @@
   bool ShouldRegisterCardWithFido(
       const CreditCardCVCAuthenticator::CVCAuthenticationResponse& response);
 
+  // Returns true if the we can offer FIDO opt-in for the user. In the
+  // downstream flow, after we offer FIDO opt-in, if the user accepts we might
+  // also offer FIDO authentication for the downstreamed card so that the FIDO
+  // registration flow is complete.
+  bool ShouldOfferFidoOptInDialog(
+      const CreditCardCVCAuthenticator::CVCAuthenticationResponse& response);
+
   // TODO(crbug.com/991037): Move this function under the build flags after the
   // refactoring is done. Offer the option to use WebAuthn for authenticating
   // future card unmasking.
diff --git a/components/autofill/core/browser/payments/full_card_request.cc b/components/autofill/core/browser/payments/full_card_request.cc
index f0bd94a..9ab7577 100644
--- a/components/autofill/core/browser/payments/full_card_request.cc
+++ b/components/autofill/core/browser/payments/full_card_request.cc
@@ -299,8 +299,6 @@
 
       const std::u16string cvc =
           (base::FeatureList::IsEnabled(
-               features::kAutofillEnableGoogleIssuedCard) ||
-           base::FeatureList::IsEnabled(
                features::kAutofillAlwaysReturnCloudTokenizedCard) ||
            base::FeatureList::IsEnabled(
                features::kAutofillEnableMerchantBoundVirtualCards)) &&
diff --git a/components/autofill/core/browser/payments/full_card_request_unittest.cc b/components/autofill/core/browser/payments/full_card_request_unittest.cc
index 6ee2c0d..e81295d 100644
--- a/components/autofill/core/browser/payments/full_card_request_unittest.cc
+++ b/components/autofill/core/browser/payments/full_card_request_unittest.cc
@@ -257,57 +257,6 @@
   card_unmask_delegate()->OnUnmaskPromptClosed();
 }
 
-// Verify getting the full PAN and the CVV for a Google issued card when FIDO is
-// used for authentication.
-TEST_F(FullCardRequestTest, GetFullCardPanAndUseCvcInUnmaskResponse) {
-  scoped_feature_list_.InitAndEnableFeature(
-      features::kAutofillEnableGoogleIssuedCard);
-  EXPECT_CALL(*result_delegate(),
-              OnFullCardRequestSucceeded(
-                  testing::Ref(*request()),
-                  CardMatches(CreditCard::FULL_SERVER_CARD, "4111"),
-                  testing::Eq(u"321")));
-  EXPECT_CALL(*ui_delegate(), ShowUnmaskPrompt(_, _, _));
-  EXPECT_CALL(*ui_delegate(), OnUnmaskVerificationResult(
-                                  AutofillClient::PaymentsRpcResult::kSuccess));
-
-  request()->GetFullCard(
-      CreditCard(CreditCard::MASKED_SERVER_CARD, "server_id"),
-      AutofillClient::UnmaskCardReason::kAutofill,
-      result_delegate()->AsWeakPtr(), ui_delegate()->AsWeakPtr());
-  CardUnmaskDelegate::UserProvidedUnmaskDetails details;
-  details.cvc = u"123";
-  card_unmask_delegate()->OnUnmaskPromptAccepted(details);
-  OnDidGetRealPanWithDcvv(AutofillClient::PaymentsRpcResult::kSuccess, "4111",
-                          "321");
-  card_unmask_delegate()->OnUnmaskPromptClosed();
-}
-
-// Verify getting the full PAN for a Google issued card when CVV is used for
-// authentication.
-TEST_F(FullCardRequestTest, GetFullCardPanWithoutCvcInUnmaskResponse) {
-  scoped_feature_list_.InitAndEnableFeature(
-      features::kAutofillEnableGoogleIssuedCard);
-  EXPECT_CALL(*result_delegate(),
-              OnFullCardRequestSucceeded(
-                  testing::Ref(*request()),
-                  CardMatches(CreditCard::FULL_SERVER_CARD, "4111"),
-                  testing::Eq(u"123")));
-  EXPECT_CALL(*ui_delegate(), ShowUnmaskPrompt(_, _, _));
-  EXPECT_CALL(*ui_delegate(), OnUnmaskVerificationResult(
-                                  AutofillClient::PaymentsRpcResult::kSuccess));
-
-  request()->GetFullCard(
-      CreditCard(CreditCard::MASKED_SERVER_CARD, "server_id"),
-      AutofillClient::UnmaskCardReason::kAutofill,
-      result_delegate()->AsWeakPtr(), ui_delegate()->AsWeakPtr());
-  CardUnmaskDelegate::UserProvidedUnmaskDetails details;
-  details.cvc = u"123";
-  card_unmask_delegate()->OnUnmaskPromptAccepted(details);
-  OnDidGetRealPan(AutofillClient::PaymentsRpcResult::kSuccess, "4111");
-  card_unmask_delegate()->OnUnmaskPromptClosed();
-}
-
 // Verify getting the full PAN for a masked server card.
 TEST_F(FullCardRequestTest, GetFullCardPanAndCvcForMaskedServerCardViaFido) {
   EXPECT_CALL(
diff --git a/components/autofill/core/browser/personal_data_manager.cc b/components/autofill/core/browser/personal_data_manager.cc
index 866f282..79e88c77 100644
--- a/components/autofill/core/browser/personal_data_manager.cc
+++ b/components/autofill/core/browser/personal_data_manager.cc
@@ -1103,12 +1103,6 @@
 
   result.reserve(server_credit_cards_.size());
   for (const auto& card : server_credit_cards_) {
-    // Do not add Google issued credit card if experiment is disabled.
-    if (card.get()->IsGoogleIssuedCard() &&
-        !base::FeatureList::IsEnabled(
-            autofill::features::kAutofillEnableGoogleIssuedCard)) {
-      continue;
-    }
     result.push_back(card.get());
   }
   return result;
@@ -1122,12 +1116,6 @@
     result.push_back(card.get());
   if (IsAutofillWalletImportEnabled()) {
     for (const auto& card : server_credit_cards_) {
-      // Do not add Google issued credit card if experiment is disabled.
-      if (card.get()->IsGoogleIssuedCard() &&
-          !base::FeatureList::IsEnabled(
-              autofill::features::kAutofillEnableGoogleIssuedCard)) {
-        continue;
-      }
       result.push_back(card.get());
     }
   }
diff --git a/components/autofill/core/browser/personal_data_manager_unittest.cc b/components/autofill/core/browser/personal_data_manager_unittest.cc
index 3034858a..30c57d0 100644
--- a/components/autofill/core/browser/personal_data_manager_unittest.cc
+++ b/components/autofill/core/browser/personal_data_manager_unittest.cc
@@ -1071,55 +1071,6 @@
   ExpectSameElements(cards, personal_data_->GetCreditCards());
 }
 
-TEST_F(PersonalDataManagerTest, DoNotAddGoogleIssuedCreditCardExpOff) {
-  base::test::ScopedFeatureList scoped_features;
-  scoped_features.InitAndDisableFeature(
-      features::kAutofillEnableGoogleIssuedCard);
-  // Set up the credit cards.
-  CreditCard credit_card0 = test::GetMaskedServerCard();
-  credit_card0.set_card_issuer(CreditCard::Issuer::ISSUER_UNKNOWN);
-  CreditCard credit_card1 = test::GetMaskedServerCardAmex();
-  credit_card1.set_card_issuer(CreditCard::Issuer::GOOGLE);
-  // Add the above cards to server_cards.
-  std::vector<CreditCard> server_cards;
-  server_cards.push_back(credit_card0);
-  server_cards.push_back(credit_card1);
-  SetServerCards(server_cards);
-
-  personal_data_->Refresh();
-  WaitForOnPersonalDataChanged();
-
-  std::vector<CreditCard*> cards;
-  // Since the flag is off, only the card with ISSUER_UNKNOWN should be
-  // returned.
-  cards.push_back(&credit_card0);
-  ExpectSameElements(cards, personal_data_->GetCreditCards());
-}
-
-TEST_F(PersonalDataManagerTest, AddGoogleIssuedCreditCard) {
-  base::test::ScopedFeatureList scoped_features;
-  scoped_features.InitAndEnableFeature(
-      features::kAutofillEnableGoogleIssuedCard);
-  // Set up the credit cards.
-  CreditCard credit_card0 = test::GetMaskedServerCard();
-  credit_card0.set_card_issuer(CreditCard::Issuer::ISSUER_UNKNOWN);
-  CreditCard credit_card1 = test::GetMaskedServerCardAmex();
-  credit_card1.set_card_issuer(CreditCard::Issuer::GOOGLE);
-  // Add the above cards to server_cards.
-  std::vector<CreditCard> server_cards;
-  server_cards.push_back(credit_card0);
-  server_cards.push_back(credit_card1);
-  SetServerCards(server_cards);
-
-  personal_data_->Refresh();
-  WaitForOnPersonalDataChanged();
-
-  std::vector<CreditCard*> cards;
-  cards.push_back(&credit_card0);
-  cards.push_back(&credit_card1);
-  ExpectSameElements(cards, personal_data_->GetCreditCards());
-}
-
 // Test that a new credit card has its basic information set.
 TEST_F(PersonalDataManagerTest, AddCreditCard_BasicInformation) {
   // Create the test clock and set the time to a specific value.
diff --git a/components/autofill/core/browser/ui/payments/card_unmask_prompt_controller_impl.cc b/components/autofill/core/browser/ui/payments/card_unmask_prompt_controller_impl.cc
index 9025837a..6463633 100644
--- a/components/autofill/core/browser/ui/payments/card_unmask_prompt_controller_impl.cc
+++ b/components/autofill/core/browser/ui/payments/card_unmask_prompt_controller_impl.cc
@@ -207,12 +207,6 @@
   return l10n_util::GetStringFUTF16(
       ids, card_.CardIdentifierStringForAutofillDisplay());
 #else
-  // For Google Pay Plex cards, show a specific message that include
-  // instructions to find the CVC for their Plex card.
-  if (card_.IsGoogleIssuedCard()) {
-    return l10n_util::GetStringUTF16(
-        IDS_AUTOFILL_CARD_UNMASK_PROMPT_INSTRUCTIONS_GOOGLE_ISSUED_CARD);
-  }
   return l10n_util::GetStringUTF16(
       card_.record_type() == autofill::CreditCard::LOCAL_CARD
           ? IDS_AUTOFILL_CARD_UNMASK_PROMPT_INSTRUCTIONS_LOCAL_CARD
diff --git a/components/autofill/core/common/autofill_features.cc b/components/autofill/core/common/autofill_features.cc
index f0018ab4..bb1f9a85 100644
--- a/components/autofill/core/common/autofill_features.cc
+++ b/components/autofill/core/common/autofill_features.cc
@@ -415,9 +415,13 @@
 
 // Controls whether Autofill may fill across origins as part of the
 // AutofillAcrossIframes experiment.
-// TODO(crbug.com/1220038): Clean up when launched.
+// TODO(crbug.com/1304721): Clean up when launched.
 const base::Feature kAutofillSharedAutofill{"AutofillSharedAutofill",
                                             base::FEATURE_DISABLED_BY_DEFAULT};
+// Relaxes the conditions under which a field is safe to fill.
+// See FormForest::GetRendererFormsOfBrowserForm() for details.
+const base::FeatureParam<bool> kAutofillSharedAutofillRelaxedParam{
+    &kAutofillSharedAutofill, "relax_shared_autofill", false};
 
 // Controls attaching the autofill type predictions to their respective
 // element in the DOM.
diff --git a/components/autofill/core/common/autofill_features.h b/components/autofill/core/common/autofill_features.h
index 74ef936..3291694c4 100644
--- a/components/autofill/core/common/autofill_features.h
+++ b/components/autofill/core/common/autofill_features.h
@@ -149,6 +149,8 @@
 COMPONENT_EXPORT(AUTOFILL)
 extern const base::Feature kAutofillSharedAutofill;
 COMPONENT_EXPORT(AUTOFILL)
+extern const base::FeatureParam<bool> kAutofillSharedAutofillRelaxedParam;
+COMPONENT_EXPORT(AUTOFILL)
 extern const base::Feature kAutofillShowTypePredictions;
 COMPONENT_EXPORT(AUTOFILL)
 extern const base::Feature kAutofillSilentProfileUpdateForInsufficientImport;
diff --git a/components/autofill/core/common/autofill_payments_features.cc b/components/autofill/core/common/autofill_payments_features.cc
index 62c47656..696361f0 100644
--- a/components/autofill/core/common/autofill_payments_features.cc
+++ b/components/autofill/core/common/autofill_payments_features.cc
@@ -54,10 +54,6 @@
 const base::Feature kAutofillCreditCardUploadFeedback{
     "AutofillCreditCardUploadFeedback", base::FEATURE_DISABLED_BY_DEFAULT};
 
-// Controls whether we show a Google-issued card in the suggestions list.
-const base::Feature kAutofillEnableGoogleIssuedCard{
-    "AutofillEnableGoogleIssuedCard", base::FEATURE_DISABLED_BY_DEFAULT};
-
 // When enabled, merchant bound virtual cards will be offered when users
 // interact with a payment form.
 const base::Feature kAutofillEnableMerchantBoundVirtualCards{
diff --git a/components/autofill/core/common/autofill_payments_features.h b/components/autofill/core/common/autofill_payments_features.h
index 6de63ea..43eaafc 100644
--- a/components/autofill/core/common/autofill_payments_features.h
+++ b/components/autofill/core/common/autofill_payments_features.h
@@ -21,7 +21,6 @@
 extern const base::Feature kAutofillAutoTriggerManualFallbackForCards;
 extern const base::Feature kAutofillCreditCardAuthentication;
 extern const base::Feature kAutofillCreditCardUploadFeedback;
-extern const base::Feature kAutofillEnableGoogleIssuedCard;
 extern const base::Feature kAutofillEnableMerchantBoundVirtualCards;
 extern const base::Feature kAutofillEnableOfferNotificationForPromoCodes;
 extern const base::Feature kAutofillEnableOffersInClankKeyboardAccessory;
diff --git a/components/autofill_assistant/android/public/java/src/org/chromium/components/autofill_assistant/AssistantAutofillCreditCard.java b/components/autofill_assistant/android/public/java/src/org/chromium/components/autofill_assistant/AssistantAutofillCreditCard.java
index 7e3fc65..7e9dff7 100644
--- a/components/autofill_assistant/android/public/java/src/org/chromium/components/autofill_assistant/AssistantAutofillCreditCard.java
+++ b/components/autofill_assistant/android/public/java/src/org/chromium/components/autofill_assistant/AssistantAutofillCreditCard.java
@@ -34,7 +34,6 @@
             put("troyCC", R.drawable.troy_card);
             put("unionPayCC", R.drawable.unionpay_card);
             put("visaCC", R.drawable.visa_card);
-            put("googleIssuedCC", R.drawable.google_pay_plex);
             put("googlePay", R.drawable.google_pay);
         }
     };
diff --git a/components/autofill_payments_strings.grdp b/components/autofill_payments_strings.grdp
index 29aa40b..d6ba1ea 100644
--- a/components/autofill_payments_strings.grdp
+++ b/components/autofill_payments_strings.grdp
@@ -325,9 +325,6 @@
     <message name="IDS_AUTOFILL_CARD_UNMASK_CVC_IMAGE_DESCRIPTION" desc="Accessible description for the CVC image. It should describe where to find the CVC on a credit card.">
       The CVC is located behind your card.
     </message>
-    <message name="IDS_AUTOFILL_CARD_UNMASK_PROMPT_INSTRUCTIONS_GOOGLE_ISSUED_CARD" desc="Text explaining what the user should do in the card unmasking dialog for a GooglePay Plex card." formatter_data="android">
-      After you confirm, card details from your Google Account will be shared with this site. Find the CVC in your Plex Account details.
-    </message>
   </if>
   <if expr="is_ios">
     <message name="IDS_AUTOFILL_CARD_UNMASK_PROMPT_TITLE" desc="Title for the credit card unmasking dialog.">
diff --git a/components/autofill_payments_strings_grdp/IDS_AUTOFILL_CARD_UNMASK_PROMPT_INSTRUCTIONS_GOOGLE_ISSUED_CARD.png.sha1 b/components/autofill_payments_strings_grdp/IDS_AUTOFILL_CARD_UNMASK_PROMPT_INSTRUCTIONS_GOOGLE_ISSUED_CARD.png.sha1
deleted file mode 100644
index c5cbdb4..0000000
--- a/components/autofill_payments_strings_grdp/IDS_AUTOFILL_CARD_UNMASK_PROMPT_INSTRUCTIONS_GOOGLE_ISSUED_CARD.png.sha1
+++ /dev/null
@@ -1 +0,0 @@
-fab9ff0c5292287ed84c9564b279cff97ae8cb25
\ No newline at end of file
diff --git a/components/autofill_strings.grdp b/components/autofill_strings.grdp
index 1e26ea6..a5551ea2 100644
--- a/components/autofill_strings.grdp
+++ b/components/autofill_strings.grdp
@@ -64,9 +64,6 @@
   <message name="IDS_AUTOFILL_CC_ELO" desc="Elo credit card name.">
     Elo
   </message>
-  <message name="IDS_AUTOFILL_CC_GOOGLE_ISSUED" desc="Prefix for the Google Plex card" formatter_data="android_java">
-    Plex
-  </message>
   <message name="IDS_AUTOFILL_CC_GOOGLE_PAY" desc="Google pay brand name">
     Google Pay
   </message>
diff --git a/components/autofill_strings_grdp/IDS_AUTOFILL_CC_GOOGLE_ISSUED.png.sha1 b/components/autofill_strings_grdp/IDS_AUTOFILL_CC_GOOGLE_ISSUED.png.sha1
deleted file mode 100644
index 2c0d265..0000000
--- a/components/autofill_strings_grdp/IDS_AUTOFILL_CC_GOOGLE_ISSUED.png.sha1
+++ /dev/null
@@ -1 +0,0 @@
-25af0f307bc74982a197c2088146485505c0b018
\ No newline at end of file
diff --git a/components/desks_storage/core/desk_sync_bridge.cc b/components/desks_storage/core/desk_sync_bridge.cc
index 56d2dea..8f38d7f 100644
--- a/components/desks_storage/core/desk_sync_bridge.cc
+++ b/components/desks_storage/core/desk_sync_bridge.cc
@@ -215,7 +215,7 @@
   if (app_id.empty())
     return nullptr;
 
-  std::unique_ptr<app_restore::AppLaunchInfo> app_launch_info =
+  auto app_launch_info =
       std::make_unique<app_restore::AppLaunchInfo>(app_id, window_id);
 
   if (app.has_display_id())
@@ -558,7 +558,7 @@
 
 // Fills an app with container and open disposition.  This is only done in the
 // specific cases of Chrome Apps and PWAs.
-void FillAppWithLaunchContianerAndOpenDisposition(
+void FillAppWithLaunchContainerAndOpenDisposition(
     const app_restore::AppRestoreData* app_restore_data,
     WorkspaceDeskSpecifics_App* out_app) {
   // If present, fills the proto's `container` field with the information stored
@@ -602,7 +602,7 @@
           pwa_window->set_title(
               base::UTF16ToUTF8(app_restore_data->title.value()));
         }
-        FillAppWithLaunchContianerAndOpenDisposition(app_restore_data, out_app);
+        FillAppWithLaunchContainerAndOpenDisposition(app_restore_data, out_app);
       }
       break;
     }
@@ -621,7 +621,7 @@
         chrome_app_window->set_title(
             base::UTF16ToUTF8(app_restore_data->title.value()));
       }
-      FillAppWithLaunchContianerAndOpenDisposition(app_restore_data, out_app);
+      FillAppWithLaunchContainerAndOpenDisposition(app_restore_data, out_app);
       break;
     }
     case apps::AppType::kArc: {
@@ -695,8 +695,7 @@
 // Convert a desk template to |app_restore::RestoreData|.
 std::unique_ptr<app_restore::RestoreData> ConvertToRestoreData(
     const sync_pb::WorkspaceDeskSpecifics& entry_proto) {
-  std::unique_ptr<app_restore::RestoreData> restore_data =
-      std::make_unique<app_restore::RestoreData>();
+  auto restore_data = std::make_unique<app_restore::RestoreData>();
 
   for (auto app_proto : entry_proto.desk().apps()) {
     std::unique_ptr<app_restore::AppLaunchInfo> app_launch_info =
@@ -776,7 +775,7 @@
       pb_entry.created_time_windows_epoch_micros());
 
   // Protobuf parsing enforces UTF-8 encoding for all strings.
-  std::unique_ptr<DeskTemplate> desk_template = std::make_unique<DeskTemplate>(
+  auto desk_template = std::make_unique<DeskTemplate>(
       uuid, ash::DeskTemplateSource::kUser, pb_entry.name(), created_time);
 
   if (pb_entry.has_updated_time_windows_epoch_micros()) {
diff --git a/components/desks_storage/core/local_desk_data_manager.cc b/components/desks_storage/core/local_desk_data_manager.cc
index 3a8c83f..3eb40c0e 100644
--- a/components/desks_storage/core/local_desk_data_manager.cc
+++ b/components/desks_storage/core/local_desk_data_manager.cc
@@ -211,10 +211,6 @@
 void LocalDeskDataManager::AddOrUpdateEntry(
     std::unique_ptr<ash::DeskTemplate> new_entry,
     DeskModel::AddOrUpdateEntryCallback callback) {
-  // When a user creates a desk template locally, the desk template has |kUser|
-  // as its source. Only user desk templates should be saved.
-  DCHECK_EQ(ash::DeskTemplateSource::kUser, new_entry->source());
-
   auto status = std::make_unique<DeskModel::AddOrUpdateEntryStatus>();
 
   task_runner_->PostTaskAndReply(
diff --git a/components/feed/core/proto/v2/store.proto b/components/feed/core/proto/v2/store.proto
index 4c4efd9..67ad56d 100644
--- a/components/feed/core/proto/v2/store.proto
+++ b/components/feed/core/proto/v2/store.proto
@@ -126,6 +126,9 @@
   // Whether personalization is enabled for Discover, as reported by the last
   // FeedQuery response.
   bool discover_personalization_enabled = 8;
+  // Count of how many times the user has followed from the main menu, so we
+  // can show appropriate user education help for the following feed.
+  int32 followed_from_web_page_menu_count = 9;
 }
 
 // A set of StreamStructures that should be applied to a stream.
diff --git a/components/feed/core/v2/api_test/feed_api_stream_unittest.cc b/components/feed/core/v2/api_test/feed_api_stream_unittest.cc
index 2eedfee5..ce09170 100644
--- a/components/feed/core/v2/api_test/feed_api_stream_unittest.cc
+++ b/components/feed/core/v2/api_test/feed_api_stream_unittest.cc
@@ -2993,6 +2993,17 @@
       "ContentSuggestions.Feed.NoticeAcknowledged.Youtube", true, 1);
 }
 
+TEST_F(FeedApiTest, FollowedFromWebPageMenuCount) {
+  // Arrange.
+  TestWebFeedSurface surface(stream_.get());
+  // Act.
+  stream_->IncrementFollowedFromWebPageMenuCount();
+  // Assert.
+  EXPECT_EQ(1, stream_->GetMetadata().followed_from_web_page_menu_count());
+  EXPECT_EQ(1, stream_->GetRequestMetadata(kWebFeedStream, false)
+                   .followed_from_web_page_menu_count);
+}
+
 // Keep instantiations at the bottom.
 INSTANTIATE_TEST_SUITE_P(FeedApiTest,
                          FeedStreamTestForAllStreamTypes,
diff --git a/components/feed/core/v2/feed_stream.cc b/components/feed/core/v2/feed_stream.cc
index 67bb71c..4dac444 100644
--- a/components/feed/core/v2/feed_stream.cc
+++ b/components/feed/core/v2/feed_stream.cc
@@ -912,6 +912,8 @@
       result.session_id = session_id;
     }
   }
+  result.followed_from_web_page_menu_count =
+      metadata_.followed_from_web_page_menu_count();
 
   DCHECK(result.session_id.empty() || result.client_instance_id.empty());
   return result;
@@ -1085,6 +1087,13 @@
   return true;
 }
 
+void FeedStream::IncrementFollowedFromWebPageMenuCount() {
+  feedstore::Metadata metadata = GetMetadata();
+  metadata.set_followed_from_web_page_menu_count(
+      metadata.followed_from_web_page_menu_count() + 1);
+  SetMetadata(std::move(metadata));
+}
+
 void FeedStream::ClearAll() {
   metrics_reporter_->OnClearAll(base::Time::Now() -
                                 GetLastFetchTime(kForYouStream));
diff --git a/components/feed/core/v2/feed_stream.h b/components/feed/core/v2/feed_stream.h
index bfa21ae..6327fc3 100644
--- a/components/feed/core/v2/feed_stream.h
+++ b/components/feed/core/v2/feed_stream.h
@@ -180,6 +180,7 @@
                        ContentOrder content_order) override;
   ContentOrder GetContentOrder(const StreamType& stream_type) override;
   ContentOrder GetContentOrderFromPrefs(const StreamType& stream_type) override;
+  void IncrementFollowedFromWebPageMenuCount() override;
 
   // offline_pages::TaskQueue::Delegate.
   void OnTaskQueueIsIdle() override;
diff --git a/components/feed/core/v2/metrics_reporter.cc b/components/feed/core/v2/metrics_reporter.cc
index 593fd05c..bc55b949 100644
--- a/components/feed/core/v2/metrics_reporter.cc
+++ b/components/feed/core/v2/metrics_reporter.cc
@@ -583,6 +583,7 @@
     case FeedUserActionType::kTappedDismissPostFollowActiveHelp:
     case FeedUserActionType::kTappedDiscoverFeedPreview:
     case FeedUserActionType::kOpenedAutoplaySettings:
+    case FeedUserActionType::kTappedFollowButton:
       // Nothing additional for these actions. Note that some of these are iOS
       // only.
 
diff --git a/components/feed/core/v2/public/common_enums.cc b/components/feed/core/v2/public/common_enums.cc
index a876d373..1041c32f 100644
--- a/components/feed/core/v2/public/common_enums.cc
+++ b/components/feed/core/v2/public/common_enums.cc
@@ -98,6 +98,8 @@
       return out << "kTappedManage";
     case FeedUserActionType::kTappedManageHidden:
       return out << "kTappedManageHidden";
+    case FeedUserActionType::kTappedFollowButton:
+      return out << "kTappedFollow";
   }
 }
 
diff --git a/components/feed/core/v2/public/common_enums.h b/components/feed/core/v2/public/common_enums.h
index e4e6aed..5e6a36d 100644
--- a/components/feed/core/v2/public/common_enums.h
+++ b/components/feed/core/v2/public/common_enums.h
@@ -123,8 +123,10 @@
   kTappedManage = 42,
   // User tapped "Hidden" in the manage intestitial.
   kTappedManageHidden = 43,
+  // User tapped the "Follow" button on the main menu.
+  kTappedFollowButton = 44,
 
-  kMaxValue = kTappedManageHidden,
+  kMaxValue = kTappedFollowButton,
 };
 
 // For testing and debugging only.
diff --git a/components/feed/core/v2/public/feed_api.h b/components/feed/core/v2/public/feed_api.h
index 3e7e45d..13b8f39 100644
--- a/components/feed/core/v2/public/feed_api.h
+++ b/components/feed/core/v2/public/feed_api.h
@@ -213,6 +213,9 @@
       const feedui::StreamUpdate& stream_update) = 0;
   // Returns the time of the last successful content fetch.
   virtual base::Time GetLastFetchTime(const StreamType& stream_type) = 0;
+  // Increase the count of the number of times the user has followed from the
+  // web page menu.
+  virtual void IncrementFollowedFromWebPageMenuCount() = 0;
 };
 
 }  // namespace feed
diff --git a/components/feed/core/v2/public/test/stub_feed_api.h b/components/feed/core/v2/public/test/stub_feed_api.h
index e117da85..f171e0a 100644
--- a/components/feed/core/v2/public/test/stub_feed_api.h
+++ b/components/feed/core/v2/public/test/stub_feed_api.h
@@ -107,6 +107,7 @@
                        ContentOrder content_order) override {}
   ContentOrder GetContentOrder(const StreamType& stream_type) override;
   ContentOrder GetContentOrderFromPrefs(const StreamType& stream_type) override;
+  void IncrementFollowedFromWebPageMenuCount() override {}
 
  private:
   StubWebFeedSubscriptions web_feed_subscriptions_;
diff --git a/components/feed/core/v2/types.h b/components/feed/core/v2/types.h
index 3973ffd..884ff3d 100644
--- a/components/feed/core/v2/types.h
+++ b/components/feed/core/v2/types.h
@@ -57,6 +57,7 @@
   ContentOrder content_order = ContentOrder::kUnspecified;
   bool notice_card_acknowledged = false;
   bool autoplay_enabled = false;
+  int followed_from_web_page_menu_count = 0;
   std::vector<std::string> acknowledged_notice_keys;
 };
 
diff --git a/components/global_media_controls/public/media_item_producer.h b/components/global_media_controls/public/media_item_producer.h
index b97b2f7f..6c21532 100644
--- a/components/global_media_controls/public/media_item_producer.h
+++ b/components/global_media_controls/public/media_item_producer.h
@@ -25,7 +25,7 @@
   virtual base::WeakPtr<media_message_center::MediaNotificationItem>
   GetMediaItem(const std::string& id) = 0;
 
-  virtual std::set<std::string> GetActiveControllableItemIds() = 0;
+  virtual std::set<std::string> GetActiveControllableItemIds() const = 0;
 
   // Returns true if the item producer has any "frozen" items, which are items
   // that were recently active with a chance to become active again.
diff --git a/components/global_media_controls/public/media_session_item_producer.cc b/components/global_media_controls/public/media_session_item_producer.cc
index 017bd45b..bb142c7 100644
--- a/components/global_media_controls/public/media_session_item_producer.cc
+++ b/components/global_media_controls/public/media_session_item_producer.cc
@@ -241,7 +241,8 @@
   return it == sessions_.end() ? nullptr : it->second.item()->GetWeakPtr();
 }
 
-std::set<std::string> MediaSessionItemProducer::GetActiveControllableItemIds() {
+std::set<std::string> MediaSessionItemProducer::GetActiveControllableItemIds()
+    const {
   return active_controllable_session_ids_;
 }
 
diff --git a/components/global_media_controls/public/media_session_item_producer.h b/components/global_media_controls/public/media_session_item_producer.h
index 840583c..c47b071 100644
--- a/components/global_media_controls/public/media_session_item_producer.h
+++ b/components/global_media_controls/public/media_session_item_producer.h
@@ -56,7 +56,7 @@
   // MediaItemProducer:
   base::WeakPtr<media_message_center::MediaNotificationItem> GetMediaItem(
       const std::string& id) override;
-  std::set<std::string> GetActiveControllableItemIds() override;
+  std::set<std::string> GetActiveControllableItemIds() const override;
   bool HasFrozenItems() override;
   void OnItemShown(const std::string& id, MediaItemUI* item_ui) override;
   bool IsItemActivelyPlaying(const std::string& id) override;
diff --git a/components/global_media_controls/public/test/mock_media_item_producer.cc b/components/global_media_controls/public/test/mock_media_item_producer.cc
index 276ccd5..920853b 100644
--- a/components/global_media_controls/public/test/mock_media_item_producer.cc
+++ b/components/global_media_controls/public/test/mock_media_item_producer.cc
@@ -51,7 +51,8 @@
   return iter->second.item.GetWeakPtr();
 }
 
-std::set<std::string> MockMediaItemProducer::GetActiveControllableItemIds() {
+std::set<std::string> MockMediaItemProducer::GetActiveControllableItemIds()
+    const {
   std::set<std::string> active_items;
   for (auto const& item_pair : items_) {
     if (item_pair.second.active)
diff --git a/components/global_media_controls/public/test/mock_media_item_producer.h b/components/global_media_controls/public/test/mock_media_item_producer.h
index cd61f35..da5de50 100644
--- a/components/global_media_controls/public/test/mock_media_item_producer.h
+++ b/components/global_media_controls/public/test/mock_media_item_producer.h
@@ -25,7 +25,7 @@
 
   base::WeakPtr<media_message_center::MediaNotificationItem> GetMediaItem(
       const std::string& id) override;
-  std::set<std::string> GetActiveControllableItemIds() override;
+  std::set<std::string> GetActiveControllableItemIds() const override;
   bool HasFrozenItems() override;
   MOCK_METHOD(void, OnItemShown, (const std::string&, MediaItemUI*));
   MOCK_METHOD(void, OnDialogDisplayed, ());
diff --git a/components/history/core/browser/history_types.h b/components/history/core/browser/history_types.h
index 6f0a354db..6eeb4e06 100644
--- a/components/history/core/browser/history_types.h
+++ b/components/history/core/browser/history_types.h
@@ -234,8 +234,10 @@
   QueryOptions& operator=(QueryOptions&&) noexcept;
   ~QueryOptions();
 
-  // The time range to search for matches in. The beginning is inclusive and
-  // the ending is exclusive. Either one (or both) may be null.
+  // The time range to search for matches in. When `visit_order` is
+  // `RECENT_FIRST`, the beginning is inclusive and the ending is exclusive.
+  // When `VisitOrder` is `OLDEST_FIRST`, vice versa. Either one (or both) may
+  // be null.
   //
   // This will match only the one recent visit of a URL. For text search
   // queries, if the URL was visited in the given time period, but has also
@@ -281,6 +283,15 @@
   // When this is true, the matching_algorithm field is ignored.
   bool host_only = false;
 
+  enum VisitOrder {
+    RECENT_FIRST,
+    OLDEST_FIRST,
+  };
+
+  // Whether to prioritize most recent or oldest visits when `max_count` is
+  // reached. Will affect visit order as well.
+  VisitOrder visit_order = RECENT_FIRST;
+
   // Helpers to get the effective parameters values, since a value of 0 means
   // "unspecified".
   int EffectiveMaxCount() const;
diff --git a/components/history/core/browser/visit_database.cc b/components/history/core/browser/visit_database.cc
index 7ae471a..fb84c67 100644
--- a/components/history/core/browser/visit_database.cc
+++ b/components/history/core/browser/visit_database.cc
@@ -356,12 +356,19 @@
                                            VisitVector* visits) {
   visits->clear();
 
-  sql::Statement statement(GetDB().GetCachedStatement(
-      SQL_FROM_HERE,
-      "SELECT" HISTORY_VISIT_ROW_FIELDS
-      "FROM visits "
-      "WHERE url=? AND visit_time >= ? AND visit_time < ? "
-      "ORDER BY visit_time DESC"));
+  sql::Statement statement;
+  if (options.visit_order == QueryOptions::RECENT_FIRST) {
+    statement.Assign(GetDB().GetCachedStatement(
+        SQL_FROM_HERE, "SELECT" HISTORY_VISIT_ROW_FIELDS "FROM visits "
+                       "WHERE url=? AND visit_time>=? AND visit_time<? "
+                       "ORDER BY visit_time DESC"));
+  } else {
+    statement.Assign(GetDB().GetCachedStatement(
+        SQL_FROM_HERE, "SELECT" HISTORY_VISIT_ROW_FIELDS "FROM visits "
+                       "WHERE url=? AND visit_time>? AND visit_time<=? "
+                       "ORDER BY visit_time ASC"));
+  }
+
   statement.BindInt64(0, url_id);
   statement.BindInt64(1, options.EffectiveBeginTime());
   statement.BindInt64(2, options.EffectiveEndTime());
@@ -454,12 +461,19 @@
   visits->clear();
   // The visit_time values can be duplicated in a redirect chain, so we sort
   // by id too, to ensure a consistent ordering just in case.
-  sql::Statement statement(GetDB().GetCachedStatement(
-      SQL_FROM_HERE,
-      "SELECT" HISTORY_VISIT_ROW_FIELDS
-      "FROM visits "
-      "WHERE visit_time >= ? AND visit_time < ? "
-      "ORDER BY visit_time DESC, id DESC"));
+
+  sql::Statement statement;
+  if (options.visit_order == QueryOptions::RECENT_FIRST) {
+    statement.Assign(GetDB().GetCachedStatement(
+        SQL_FROM_HERE, "SELECT" HISTORY_VISIT_ROW_FIELDS "FROM visits "
+                       "WHERE visit_time>=? AND visit_time<? "
+                       "ORDER BY visit_time DESC, id DESC"));
+  } else {
+    statement.Assign(GetDB().GetCachedStatement(
+        SQL_FROM_HERE, "SELECT" HISTORY_VISIT_ROW_FIELDS "FROM visits "
+                       "WHERE visit_time>? AND visit_time<=? "
+                       "ORDER BY visit_time ASC, id DESC"));
+  }
 
   statement.BindInt64(0, options.EffectiveBeginTime());
   statement.BindInt64(1, options.EffectiveEndTime());
diff --git a/components/history/core/browser/visit_database_unittest.cc b/components/history/core/browser/visit_database_unittest.cc
index 5f58f7e..fcf7ead 100644
--- a/components/history/core/browser/visit_database_unittest.cc
+++ b/components/history/core/browser/visit_database_unittest.cc
@@ -327,7 +327,7 @@
   ASSERT_EQ(static_cast<size_t>(1), results.size());
   EXPECT_TRUE(IsVisitInfoEqual(results[0], test_visit_rows[0]));
 
-  options = QueryOptions();  // Reset to options to default.
+  options = QueryOptions();  // Reset options to default.
 
   // Query for a max count and make sure we get only that number.
   options.max_count = 1;
@@ -343,6 +343,13 @@
   GetVisibleVisitsInRange(options, &results);
   ASSERT_EQ(static_cast<size_t>(1), results.size());
   EXPECT_TRUE(IsVisitInfoEqual(results[0], test_visit_rows[1]));
+
+  // Query oldest visits in a time range and make sure beginning is exclusive
+  // and ending is inclusive.
+  options.visit_order = QueryOptions::OLDEST_FIRST;
+  GetVisibleVisitsInRange(options, &results);
+  ASSERT_EQ(static_cast<size_t>(1), results.size());
+  EXPECT_TRUE(IsVisitInfoEqual(results[0], test_visit_rows[3]));
 }
 
 TEST_F(VisitDatabaseTest, GetAllURLIDsForTransition) {
@@ -424,6 +431,40 @@
   EXPECT_TRUE(IsVisitInfoEqual(results[0], test_visit_rows[5]));
   EXPECT_TRUE(IsVisitInfoEqual(results[1], test_visit_rows[1]));
   EXPECT_TRUE(IsVisitInfoEqual(results[2], test_visit_rows[0]));
+
+  // Now try with a `max_count` limit to get the newest 2 visits only.
+  options.max_count = 2;
+  GetVisibleVisitsForURL(url_id, options, &results);
+  ASSERT_EQ(static_cast<size_t>(2), results.size());
+  EXPECT_TRUE(IsVisitInfoEqual(results[0], test_visit_rows[5]));
+  EXPECT_TRUE(IsVisitInfoEqual(results[1], test_visit_rows[1]));
+
+  // Now try getting the oldest 2 visits and make sure they're ordered oldest
+  // first.
+  options.visit_order = QueryOptions::OLDEST_FIRST;
+  GetVisibleVisitsForURL(url_id, options, &results);
+  ASSERT_EQ(static_cast<size_t>(2), results.size());
+  EXPECT_TRUE(IsVisitInfoEqual(results[0], test_visit_rows[0]));
+  EXPECT_TRUE(IsVisitInfoEqual(results[1], test_visit_rows[1]));
+
+  // Query a time range and make sure beginning is inclusive and ending is
+  // exclusive.
+  options.begin_time = test_visit_rows[0].visit_time;
+  options.end_time = test_visit_rows[5].visit_time;
+  options.visit_order = QueryOptions::RECENT_FIRST;
+  options.max_count = 0;
+  GetVisibleVisitsForURL(url_id, options, &results);
+  ASSERT_EQ(static_cast<size_t>(2), results.size());
+  EXPECT_TRUE(IsVisitInfoEqual(results[0], test_visit_rows[1]));
+  EXPECT_TRUE(IsVisitInfoEqual(results[1], test_visit_rows[0]));
+
+  // Query oldest visits in a time range and make sure beginning is exclusive
+  // and ending is inclusive.
+  options.visit_order = QueryOptions::OLDEST_FIRST;
+  GetVisibleVisitsForURL(url_id, options, &results);
+  ASSERT_EQ(static_cast<size_t>(2), results.size());
+  EXPECT_TRUE(IsVisitInfoEqual(results[0], test_visit_rows[1]));
+  EXPECT_TRUE(IsVisitInfoEqual(results[1], test_visit_rows[5]));
 }
 
 TEST_F(VisitDatabaseTest, GetHistoryCount) {
diff --git a/components/history_clusters/core/config.cc b/components/history_clusters/core/config.cc
index b1cfe3e2..1a6ff10 100644
--- a/components/history_clusters/core/config.cc
+++ b/components/history_clusters/core/config.cc
@@ -213,6 +213,9 @@
                                  base::WhitespaceHandling::TRIM_WHITESPACE,
                                  base::SplitResult::SPLIT_WANT_NONEMPTY);
   hosts_to_skip_clustering_for = {hosts.begin(), hosts.end()};
+
+  use_continue_on_shutdown = base::FeatureList::IsEnabled(
+      internal::kHistoryClustersUseContinueOnShutdown);
 }
 
 Config::Config(const Config& other) = default;
diff --git a/components/history_clusters/core/config.h b/components/history_clusters/core/config.h
index 07bffa23..6ae38c8 100644
--- a/components/history_clusters/core/config.h
+++ b/components/history_clusters/core/config.h
@@ -194,6 +194,9 @@
   // any cluster.
   base::flat_set<std::string> hosts_to_skip_clustering_for;
 
+  // True if the task runner should use trait CONTINUE_ON_SHUTDOWN.
+  bool use_continue_on_shutdown = true;
+
   Config();
   Config(const Config& other);
   ~Config();
diff --git a/components/history_clusters/core/features.cc b/components/history_clusters/core/features.cc
index 44e9130..cd19bda 100644
--- a/components/history_clusters/core/features.cc
+++ b/components/history_clusters/core/features.cc
@@ -45,6 +45,9 @@
 const base::Feature kHistoryClustersInternalsPage{
     "HistoryClustersInternalsPage", base::FEATURE_DISABLED_BY_DEFAULT};
 
+const base::Feature kHistoryClustersUseContinueOnShutdown{
+    "HistoryClustersUseContinueOnShutdown", base::FEATURE_ENABLED_BY_DEFAULT};
+
 }  // namespace internal
 
 }  // namespace history_clusters
diff --git a/components/history_clusters/core/features.h b/components/history_clusters/core/features.h
index 8ef3b998..c84cf422c 100644
--- a/components/history_clusters/core/features.h
+++ b/components/history_clusters/core/features.h
@@ -43,6 +43,9 @@
 // Enables the history clusters internals page.
 extern const base::Feature kHistoryClustersInternalsPage;
 
+// Enables use of task runner with trait CONTINUE_ON_SHUTDOWN.
+extern const base::Feature kHistoryClustersUseContinueOnShutdown;
+
 }  // namespace internal
 
 }  // namespace history_clusters
diff --git a/components/history_clusters/core/on_device_clustering_backend.cc b/components/history_clusters/core/on_device_clustering_backend.cc
index 9c25f81..bc8ac3ab 100644
--- a/components/history_clusters/core/on_device_clustering_backend.cc
+++ b/components/history_clusters/core/on_device_clustering_backend.cc
@@ -90,12 +90,26 @@
     : template_url_service_(template_url_service),
       entity_metadata_provider_(entity_metadata_provider),
       engagement_score_provider_(engagement_score_provider),
+      user_visible_task_traits_(
+          {base::MayBlock(), base::TaskPriority::USER_VISIBLE}),
+      continue_on_shutdown_user_visible_task_traits_(
+          {base::MayBlock(), base::TaskPriority::USER_VISIBLE,
+           base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN}),
       user_visible_priority_background_task_runner_(
           base::ThreadPool::CreateSequencedTaskRunner(
-              {base::MayBlock(), base::TaskPriority::USER_VISIBLE})),
+              GetConfig().use_continue_on_shutdown
+                  ? continue_on_shutdown_user_visible_task_traits_
+                  : user_visible_task_traits_)),
+      best_effort_task_traits_(
+          {base::MayBlock(), base::TaskPriority::BEST_EFFORT}),
+      continue_on_shutdown_best_effort_task_traits_(
+          {base::MayBlock(), base::TaskPriority::BEST_EFFORT,
+           base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN}),
       best_effort_priority_background_task_runner_(
           base::ThreadPool::CreateSequencedTaskRunner(
-              {base::MayBlock(), base::TaskPriority::BEST_EFFORT})),
+              GetConfig().use_continue_on_shutdown
+                  ? continue_on_shutdown_best_effort_task_traits_
+                  : best_effort_task_traits_)),
       engagement_score_cache_last_refresh_timestamp_(base::TimeTicks::Now()),
       engagement_score_cache_(
           GetFieldTrialParamByFeatureAsInt(features::kUseEngagementScoreCache,
diff --git a/components/history_clusters/core/on_device_clustering_backend.h b/components/history_clusters/core/on_device_clustering_backend.h
index acbe1b5..4882967 100644
--- a/components/history_clusters/core/on_device_clustering_backend.h
+++ b/components/history_clusters/core/on_device_clustering_backend.h
@@ -13,6 +13,7 @@
 #include "base/memory/weak_ptr.h"
 #include "base/sequence_checker.h"
 #include "base/task/sequenced_task_runner.h"
+#include "base/task/task_traits.h"
 #include "components/history_clusters/core/cluster_finalizer.h"
 #include "components/history_clusters/core/cluster_processor.h"
 #include "components/history_clusters/core/clusterer.h"
@@ -102,8 +103,12 @@
   // The task runners to run clustering passes on.
   // |user_visible_priority_background_task_runner_| should be used iff
   // clustering is blocking content on a page that user is actively looking at.
+  const base::TaskTraits user_visible_task_traits_;
+  const base::TaskTraits continue_on_shutdown_user_visible_task_traits_;
   scoped_refptr<base::SequencedTaskRunner>
       user_visible_priority_background_task_runner_;
+  const base::TaskTraits best_effort_task_traits_;
+  const base::TaskTraits continue_on_shutdown_best_effort_task_traits_;
   scoped_refptr<base::SequencedTaskRunner>
       best_effort_priority_background_task_runner_;
 
diff --git a/components/media_router/common/media_route.h b/components/media_router/common/media_route.h
index 3f3f0ac..4a9a166 100644
--- a/components/media_router/common/media_route.h
+++ b/components/media_router/common/media_route.h
@@ -143,6 +143,7 @@
 
   // |true| if the presentation associated with this route is a local
   // presentation.
+  // TODO(crbug.com/1309770): Remove |is_local_presentation_|.
   bool is_local_presentation_ = false;
 
   // |true| if the route is created by the MRP but is waiting for receivers'
diff --git a/components/media_router/common/pref_names.cc b/components/media_router/common/pref_names.cc
index 87a4e34..936855f 100644
--- a/components/media_router/common/pref_names.cc
+++ b/components/media_router/common/pref_names.cc
@@ -13,6 +13,11 @@
 // A list of website origins on which the user has chosen to use tab mirroring.
 const char kMediaRouterTabMirroringSources[] =
     "media_router.tab_mirroring_sources";
+// Whether or not the user has enabled to show Cast sessions started by
+// other devices on the same network. This change only affects the Zenith
+// dialog. Defaults to true.
+const char kMediaRouterShowCastSessionsStartedByOtherDevices[] =
+    "media_router.show_cast_sessions_started_by_other_devices.enabled";
 
 }  // namespace prefs
 }  // namespace media_router
diff --git a/components/media_router/common/pref_names.h b/components/media_router/common/pref_names.h
index dc60725..0b776dd 100644
--- a/components/media_router/common/pref_names.h
+++ b/components/media_router/common/pref_names.h
@@ -10,6 +10,7 @@
 
 extern const char kMediaRouterMediaRemotingEnabled[];
 extern const char kMediaRouterTabMirroringSources[];
+extern const char kMediaRouterShowCastSessionsStartedByOtherDevices[];
 
 }  // namespace prefs
 }  // namespace media_router
diff --git a/components/optimization_guide/content/browser/page_content_annotations_model_manager.cc b/components/optimization_guide/content/browser/page_content_annotations_model_manager.cc
index 6637e99..7568a9d 100644
--- a/components/optimization_guide/content/browser/page_content_annotations_model_manager.cc
+++ b/components/optimization_guide/content/browser/page_content_annotations_model_manager.cc
@@ -4,6 +4,7 @@
 
 #include "components/optimization_guide/content/browser/page_content_annotations_model_manager.h"
 
+#include "base/feature_list.h"
 #include "base/metrics/histogram_functions.h"
 #include "base/metrics/histogram_macros_local.h"
 #include "base/strings/string_number_conversions.h"
@@ -123,9 +124,22 @@
       "PageEntitiesModelRequested",
       true);
 #if BUILDFLAG(BUILD_WITH_INTERNAL_OPTIMIZATION_GUIDE)
+
+  base::TaskTraits task_traits = {base::MayBlock(),
+                                  base::TaskPriority::BEST_EFFORT};
+  if (base::FeatureList::IsEnabled(
+          features::
+              kOptimizationGuideUseContinueOnShutdownForPageContentAnnotations)) {
+    task_traits = {base::MayBlock(), base::TaskPriority::BEST_EFFORT,
+                   base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN};
+  }
+
+  scoped_refptr<base::SequencedTaskRunner> background_task_runner =
+      base::ThreadPool::CreateSequencedTaskRunner(task_traits);
+
   page_entities_model_executor_ =
       std::make_unique<PageEntitiesModelExecutorImpl>(
-          optimization_guide_model_provider);
+          optimization_guide_model_provider, background_task_runner);
 #endif
 }
 
@@ -212,10 +226,18 @@
       proto::PAGE_TOPICS_SUPPORTED_OUTPUT_CATEGORIES);
   page_topics_model_metadata.SerializeToString(model_metadata.mutable_value());
 
+  base::TaskTraits task_traits = {base::MayBlock(),
+                                  base::TaskPriority::BEST_EFFORT};
+  if (base::FeatureList::IsEnabled(
+          features::
+              kOptimizationGuideUseContinueOnShutdownForPageContentAnnotations)) {
+    task_traits = {base::MayBlock(), base::TaskPriority::BEST_EFFORT,
+                   base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN};
+  }
+
   page_topics_model_handler_ = std::make_unique<BertModelHandler>(
       optimization_guide_model_provider,
-      base::ThreadPool::CreateSequencedTaskRunner(
-          {base::MayBlock(), base::TaskPriority::BEST_EFFORT}),
+      base::ThreadPool::CreateSequencedTaskRunner(task_traits),
       proto::OPTIMIZATION_TARGET_PAGE_TOPICS, model_metadata);
 }
 
diff --git a/components/optimization_guide/core/optimization_guide_features.cc b/components/optimization_guide/core/optimization_guide_features.cc
index b42818e..37ba4fc 100644
--- a/components/optimization_guide/core/optimization_guide_features.cc
+++ b/components/optimization_guide/core/optimization_guide_features.cc
@@ -151,6 +151,11 @@
 const base::Feature kPreventLongRunningPredictionModels{
     "PreventLongRunningPredictionModels", base::FEATURE_DISABLED_BY_DEFAULT};
 
+const base::Feature
+    kOptimizationGuideUseContinueOnShutdownForPageContentAnnotations{
+        "OptimizationGuideUseContinueOnShutdownForPageContentAnnotations",
+        base::FEATURE_ENABLED_BY_DEFAULT};
+
 // The default value here is a bit of a guess.
 // TODO(crbug/1163244): This should be tuned once metrics are available.
 base::TimeDelta PageTextExtractionOutstandingRequestsGracePeriod() {
diff --git a/components/optimization_guide/core/optimization_guide_features.h b/components/optimization_guide/core/optimization_guide_features.h
index f90021a1..d17d191 100644
--- a/components/optimization_guide/core/optimization_guide_features.h
+++ b/components/optimization_guide/core/optimization_guide_features.h
@@ -43,6 +43,11 @@
 extern const base::Feature kBatchAnnotationsValidation;
 extern const base::Feature kPreventLongRunningPredictionModels;
 
+// Enables use of task runner with trait CONTINUE_ON_SHUTDOWN for page content
+// annotations on-device models.
+extern const base::Feature
+    kOptimizationGuideUseContinueOnShutdownForPageContentAnnotations;
+
 // The grace period duration for how long to give outstanding page text dump
 // requests to respond after DidFinishLoad.
 base::TimeDelta PageTextExtractionOutstandingRequestsGracePeriod();
diff --git a/components/optimization_guide/core/page_entities_model_executor_impl.h b/components/optimization_guide/core/page_entities_model_executor_impl.h
index d39944fc2..1f12d294 100644
--- a/components/optimization_guide/core/page_entities_model_executor_impl.h
+++ b/components/optimization_guide/core/page_entities_model_executor_impl.h
@@ -75,9 +75,7 @@
  public:
   PageEntitiesModelExecutorImpl(
       OptimizationGuideModelProvider* model_provider,
-      scoped_refptr<base::SequencedTaskRunner> background_task_runner =
-          base::ThreadPool::CreateSequencedTaskRunner(
-              {base::MayBlock(), base::TaskPriority::BEST_EFFORT}));
+      scoped_refptr<base::SequencedTaskRunner> background_task_runner);
   ~PageEntitiesModelExecutorImpl() override;
   PageEntitiesModelExecutorImpl(const PageEntitiesModelExecutorImpl&) = delete;
   PageEntitiesModelExecutorImpl& operator=(
diff --git a/components/optimization_guide/core/page_entities_model_executor_impl_unittest.cc b/components/optimization_guide/core/page_entities_model_executor_impl_unittest.cc
index e195445..96f8ca80 100644
--- a/components/optimization_guide/core/page_entities_model_executor_impl_unittest.cc
+++ b/components/optimization_guide/core/page_entities_model_executor_impl_unittest.cc
@@ -64,8 +64,11 @@
  public:
   void SetUp() override {
     model_observer_tracker_ = std::make_unique<ModelObserverTracker>();
+
     model_executor_ = std::make_unique<PageEntitiesModelExecutorImpl>(
-        model_observer_tracker_.get());
+        model_observer_tracker_.get(),
+        base::ThreadPool::CreateSequencedTaskRunner(
+            {base::MayBlock(), base::TaskPriority::BEST_EFFORT}));
 
     // Wait for PageEntitiesModelExecutor to set everything up.
     task_environment_.RunUntilIdle();
diff --git a/components/resources/autofill_scaled_resources.grdp b/components/resources/autofill_scaled_resources.grdp
index 7706b25..5661022 100644
--- a/components/resources/autofill_scaled_resources.grdp
+++ b/components/resources/autofill_scaled_resources.grdp
@@ -15,11 +15,9 @@
   <structure type="chrome_scaled_image" name="IDR_AUTOFILL_VIRTUAL_CARD_ENROLL_DIALOG_DARK" file="autofill/virtual_card_enroll_dark.png" />
   <if expr="_google_chrome">
     <then>
-      <structure type="chrome_scaled_image" name="IDR_AUTOFILL_GOOGLE_ISSUED_CARD" file="google_chrome/autofill/googlepay_plex.png" />
       <structure type="chrome_scaled_image" name="IDR_AUTOFILL_GOOGLE_PAY" file="google_chrome/autofill/googlepay.png" />
     </then>
     <else>
-      <structure type="chrome_scaled_image" name="IDR_AUTOFILL_GOOGLE_ISSUED_CARD" file="autofill/cc-generic.png" />
       <structure type="chrome_scaled_image" name="IDR_AUTOFILL_GOOGLE_PAY" file="autofill/cc-generic.png" />
     </else>
   </if>
diff --git a/components/segmentation_platform/components_unittests.filter b/components/segmentation_platform/components_unittests.filter
index 31e8d1d..3addc0e 100644
--- a/components/segmentation_platform/components_unittests.filter
+++ b/components/segmentation_platform/components_unittests.filter
@@ -32,6 +32,7 @@
 TrainingDataCollectorImplTest.*
 UkmConfigTest.*
 UkmDatabaseBackendTest.*
+UkmDataManagerImplTest.*
 UkmMetricsTableTest.*
 UkmObserverTest.*
 UkmUrlTableTest.*
diff --git a/components/segmentation_platform/internal/BUILD.gn b/components/segmentation_platform/internal/BUILD.gn
index 60ee5cb..4ded4ec 100644
--- a/components/segmentation_platform/internal/BUILD.gn
+++ b/components/segmentation_platform/internal/BUILD.gn
@@ -221,6 +221,8 @@
     "mock_ukm_data_manager.h",
     "scheduler/model_execution_scheduler_unittest.cc",
     "segmentation_platform_service_impl_unittest.cc",
+    "segmentation_platform_service_test_base.cc",
+    "segmentation_platform_service_test_base.h",
     "segmentation_ukm_helper_unittest.cc",
     "selection/segment_result_provider_unittest.cc",
     "selection/segment_score_provider_unittest.cc",
@@ -244,6 +246,7 @@
     "//base",
     "//base/test:test_support",
     "//components/history/core/browser:browser",
+    "//components/history/core/test",
     "//components/leveldb_proto:test_support",
     "//components/optimization_guide/core",
     "//components/optimization_guide/core:test_support",
@@ -264,10 +267,13 @@
   if (build_with_tflite_lib) {
     # IMPORTANT NOTE: When adding new tests, also remember to update the list of
     # tests in //components/segmentation_platform/components_unittests.filter
+    # TODO(ssid): Move all these tests out of tflite check once
+    # model_execution_manager is not tflite dependent.
     sources += [
       "execution/model_execution_manager_impl_unittest.cc",
       "execution/optimization_guide/optimization_guide_segmentation_model_provider_unittest.cc",
       "execution/optimization_guide/segmentation_model_executor_unittest.cc",
+      "ukm_data_manager_impl_unittest.cc",
     ]
     deps += [
       ":optimization_guide_segmentation_handler",
diff --git a/components/segmentation_platform/internal/database/database_maintenance_impl.cc b/components/segmentation_platform/internal/database/database_maintenance_impl.cc
index c404786..c81318c4 100644
--- a/components/segmentation_platform/internal/database/database_maintenance_impl.cc
+++ b/components/segmentation_platform/internal/database/database_maintenance_impl.cc
@@ -40,10 +40,10 @@
 
 namespace {
 std::set<SignalIdentifier> CollectAllSignalIdentifiers(
-    const SegmentInfoDatabase::SegmentInfoList& segment_infos) {
+    const DefaultModelManager::SegmentInfoList& segment_infos) {
   std::set<SignalIdentifier> signal_ids;
-  for (const auto& pair : segment_infos) {
-    const proto::SegmentInfo& segment_info = pair.second;
+  for (const auto& info : segment_infos) {
+    const proto::SegmentInfo& segment_info = info->segment_info;
     const auto& metadata = segment_info.model_metadata();
     auto features =
         metadata_utils::GetAllUmaFeatures(metadata, /*include_outputs=*/true);
@@ -115,9 +115,9 @@
 }
 
 void DatabaseMaintenanceImpl::OnSegmentInfoCallback(
-    std::unique_ptr<SegmentInfoDatabase::SegmentInfoList> segment_infos) {
+    DefaultModelManager::SegmentInfoList segment_infos) {
   std::set<SignalIdentifier> signal_ids =
-      CollectAllSignalIdentifiers(*segment_infos);
+      CollectAllSignalIdentifiers(segment_infos);
   stats::RecordMaintenanceSignalIdentifierCount(signal_ids.size());
 
   auto all_tasks = GetAllTasks(signal_ids);
diff --git a/components/segmentation_platform/internal/database/database_maintenance_impl.h b/components/segmentation_platform/internal/database/database_maintenance_impl.h
index dbba9c72..ea70a01 100644
--- a/components/segmentation_platform/internal/database/database_maintenance_impl.h
+++ b/components/segmentation_platform/internal/database/database_maintenance_impl.h
@@ -16,7 +16,7 @@
 #include "base/memory/weak_ptr.h"
 #include "components/optimization_guide/proto/models.pb.h"
 #include "components/segmentation_platform/internal/database/database_maintenance.h"
-#include "components/segmentation_platform/internal/database/segment_info_database.h"
+#include "components/segmentation_platform/internal/execution/default_model_manager.h"
 #include "components/segmentation_platform/internal/proto/types.pb.h"
 
 namespace base {
@@ -28,6 +28,7 @@
 
 namespace segmentation_platform {
 class DefaultModelManager;
+class SegmentInfoDatabase;
 class SignalDatabase;
 class SignalStorageConfig;
 
@@ -58,7 +59,7 @@
   // All tasks currently need information about various segments, so this is
   // the callback after the initial database lookup for this data.
   void OnSegmentInfoCallback(
-      std::unique_ptr<SegmentInfoDatabase::SegmentInfoList> segment_infos);
+      DefaultModelManager::SegmentInfoList segment_infos);
 
   // Returns an ordered vector of all the tasks we are supposed to perform.
   // These are unfinished and also need to be linked to the next task to be
diff --git a/components/segmentation_platform/internal/database/database_maintenance_impl_unittest.cc b/components/segmentation_platform/internal/database/database_maintenance_impl_unittest.cc
index 2b37501..8c7bc74 100644
--- a/components/segmentation_platform/internal/database/database_maintenance_impl_unittest.cc
+++ b/components/segmentation_platform/internal/database/database_maintenance_impl_unittest.cc
@@ -71,18 +71,30 @@
       const std::vector<OptimizationTarget>& segment_ids,
       MultipleSegmentInfoCallback callback) override {
     base::ThreadTaskRunnerHandle::Get()->PostTask(
-        FROM_HERE,
-        base::BindOnce(
-            std::move(callback),
-            std::make_unique<DefaultModelManager::SegmentInfoList>()));
+        FROM_HERE, base::BindOnce(std::move(callback),
+                                  DefaultModelManager::SegmentInfoList()));
   }
 
   void GetAllSegmentInfoFromBothModels(
       const std::vector<OptimizationTarget>& segment_ids,
       SegmentInfoDatabase* segment_database,
       MultipleSegmentInfoCallback callback) override {
-    segment_database->GetSegmentInfoForSegments(segment_ids,
-                                                std::move(callback));
+    segment_database->GetSegmentInfoForSegments(
+        segment_ids,
+        base::BindOnce(
+            [](DefaultModelManager::MultipleSegmentInfoCallback callback,
+               std::unique_ptr<SegmentInfoDatabase::SegmentInfoList> db_list) {
+              DefaultModelManager::SegmentInfoList list;
+              for (auto& pair : *db_list) {
+                list.push_back(std::make_unique<
+                               DefaultModelManager::SegmentInfoWrapper>());
+                list.back()->segment_source =
+                    DefaultModelManager::SegmentSource::DATABASE;
+                list.back()->segment_info.Swap(&pair.second);
+              }
+              std::move(callback).Run(std::move(list));
+            },
+            std::move(callback)));
   }
 };
 
diff --git a/components/segmentation_platform/internal/database/metadata_utils.cc b/components/segmentation_platform/internal/database/metadata_utils.cc
index 0f68c5a..7cd8209 100644
--- a/components/segmentation_platform/internal/database/metadata_utils.cc
+++ b/components/segmentation_platform/internal/database/metadata_utils.cc
@@ -177,6 +177,10 @@
       auto feature_result = ValidateMetadataCustomInput(feature.custom_input());
       if (feature_result != ValidationResult::kValidationSuccess)
         return feature_result;
+    } else if (feature.has_sql_feature()) {
+      // TODO(haileywang): Fix sql validation with other requirements.
+      if (feature.sql_feature().sql().empty())
+        return ValidationResult::kFeatureListInvalid;
     } else {
       return ValidationResult::kFeatureListInvalid;
     }
diff --git a/components/segmentation_platform/internal/execution/default_model_manager.cc b/components/segmentation_platform/internal/execution/default_model_manager.cc
index 20ab094d..8c10c5d 100644
--- a/components/segmentation_platform/internal/execution/default_model_manager.cc
+++ b/components/segmentation_platform/internal/execution/default_model_manager.cc
@@ -9,6 +9,9 @@
 
 namespace segmentation_platform {
 
+DefaultModelManager::SegmentInfoWrapper::SegmentInfoWrapper() = default;
+DefaultModelManager::SegmentInfoWrapper::~SegmentInfoWrapper() = default;
+
 DefaultModelManager::DefaultModelManager(
     ModelProviderFactory* model_provider_factory,
     const std::vector<OptimizationTarget>& segment_ids)
@@ -66,7 +69,7 @@
   if (!default_provider) {
     // If there are no more default providers, return the result so far.
     base::ThreadTaskRunnerHandle::Get()->PostTask(
-        FROM_HERE, base::BindOnce(std::move(callback), std::move(result)));
+        FROM_HERE, base::BindOnce(std::move(callback), std::move(*result)));
     return;
   }
 
@@ -82,11 +85,12 @@
     OptimizationTarget segment_id,
     proto::SegmentationModelMetadata metadata,
     int64_t model_version) {
-  proto::SegmentInfo segment_info;
-  segment_info.set_segment_id(segment_id);
-  segment_info.mutable_model_metadata()->CopyFrom(metadata);
-  segment_info.set_model_version(model_version);
-  result->push_back(std::make_pair(segment_id, segment_info));
+  auto info = std::make_unique<SegmentInfoWrapper>();
+  info->segment_source = DefaultModelManager::SegmentSource::DEFAULT_MODEL;
+  info->segment_info.set_segment_id(segment_id);
+  info->segment_info.mutable_model_metadata()->CopyFrom(metadata);
+  info->segment_info.set_model_version(model_version);
+  result->push_back(std::move(info));
 
   GetNextSegmentInfoFromDefaultModel(
       std::move(result), std::move(remaining_segment_ids), std::move(callback));
@@ -117,12 +121,19 @@
 void DefaultModelManager::OnGetAllSegmentInfoFromDefaultModel(
     MultipleSegmentInfoCallback callback,
     std::unique_ptr<SegmentInfoDatabase::SegmentInfoList> segment_infos_from_db,
-    std::unique_ptr<SegmentInfoDatabase::SegmentInfoList>
-        segment_infos_from_default_model) {
-  std::unique_ptr<SegmentInfoDatabase::SegmentInfoList> merged_results =
-      std::move(segment_infos_from_db);
-  for (const auto& segment_info : *segment_infos_from_default_model)
-    merged_results->push_back(std::move(segment_info));
+    SegmentInfoList segment_infos_from_default_model) {
+  SegmentInfoList merged_results;
+  if (segment_infos_from_db) {
+    for (auto it : *segment_infos_from_db) {
+      merged_results.push_back(std::make_unique<SegmentInfoWrapper>());
+      merged_results.back()->segment_source = SegmentSource::DATABASE;
+      merged_results.back()->segment_info.Swap(&it.second);
+    }
+  }
+  merged_results.insert(
+      merged_results.end(),
+      std::make_move_iterator(segment_infos_from_default_model.begin()),
+      std::make_move_iterator(segment_infos_from_default_model.end()));
 
   std::move(callback).Run(std::move(merged_results));
 }
diff --git a/components/segmentation_platform/internal/execution/default_model_manager.h b/components/segmentation_platform/internal/execution/default_model_manager.h
index c66a0ad..911a31cc 100644
--- a/components/segmentation_platform/internal/execution/default_model_manager.h
+++ b/components/segmentation_platform/internal/execution/default_model_manager.h
@@ -14,6 +14,7 @@
 #include "base/callback.h"
 #include "base/containers/flat_map.h"
 #include "base/logging.h"
+#include "components/segmentation_platform/internal/database/segment_info_database.h"
 #include "components/segmentation_platform/internal/proto/model_metadata.pb.h"
 #include "components/segmentation_platform/internal/proto/model_prediction.pb.h"
 #include "components/segmentation_platform/public/model_provider.h"
@@ -39,10 +40,21 @@
 
   // Callback for returning a list of segment infos associated with IDs.
   // The same segment ID can be repeated multiple times.
-  using SegmentInfoList =
-      std::vector<std::pair<OptimizationTarget, proto::SegmentInfo>>;
-  using MultipleSegmentInfoCallback =
-      base::OnceCallback<void(std::unique_ptr<SegmentInfoList>)>;
+  enum class SegmentSource {
+    DATABASE,
+    DEFAULT_MODEL,
+  };
+  struct SegmentInfoWrapper {
+    SegmentInfoWrapper();
+    ~SegmentInfoWrapper();
+    SegmentInfoWrapper(const SegmentInfoWrapper&) = delete;
+    SegmentInfoWrapper& operator=(const SegmentInfoWrapper&) = delete;
+
+    SegmentSource segment_source;
+    proto::SegmentInfo segment_info;
+  };
+  using SegmentInfoList = std::vector<std::unique_ptr<SegmentInfoWrapper>>;
+  using MultipleSegmentInfoCallback = base::OnceCallback<void(SegmentInfoList)>;
 
   // Utility function to get the segment info from both the database and the
   // default model for a given set of segment IDs. The result can contain
@@ -80,12 +92,13 @@
   void OnGetAllSegmentInfoFromDatabase(
       const std::vector<OptimizationTarget>& segment_ids,
       MultipleSegmentInfoCallback callback,
-      std::unique_ptr<SegmentInfoList> segment_infos);
+      std::unique_ptr<SegmentInfoDatabase::SegmentInfoList> segment_infos);
 
   void OnGetAllSegmentInfoFromDefaultModel(
       MultipleSegmentInfoCallback callback,
-      std::unique_ptr<SegmentInfoList> segment_infos_from_db,
-      std::unique_ptr<SegmentInfoList> segment_infos_from_default_model);
+      std::unique_ptr<SegmentInfoDatabase::SegmentInfoList>
+          segment_infos_from_db,
+      SegmentInfoList segment_infos_from_default_model);
 
   // Default model providers.
   std::map<OptimizationTarget, std::unique_ptr<ModelProvider>>
diff --git a/components/segmentation_platform/internal/execution/default_model_manager_unittest.cc b/components/segmentation_platform/internal/execution/default_model_manager_unittest.cc
index f54d09e5..c7979cc4 100644
--- a/components/segmentation_platform/internal/execution/default_model_manager_unittest.cc
+++ b/components/segmentation_platform/internal/execution/default_model_manager_unittest.cc
@@ -34,13 +34,12 @@
                 .second;
   }
 
-  void OnGetAllSegments(
-      std::unique_ptr<SegmentInfoDatabase::SegmentInfoList> entries) {
+  void OnGetAllSegments(DefaultModelManager::SegmentInfoList entries) {
     get_all_segment_result_.swap(entries);
   }
 
-  const SegmentInfoDatabase::SegmentInfoList& get_all_segment_result() const {
-    return *get_all_segment_result_;
+  const DefaultModelManager::SegmentInfoList& get_all_segment_result() const {
+    return get_all_segment_result_;
   }
 
   base::test::TaskEnvironment task_environment_;
@@ -48,7 +47,7 @@
   TestModelProviderFactory::Data model_provider_data_;
   TestModelProviderFactory model_provider_factory_;
   std::unique_ptr<DefaultModelManager> default_model_manager_;
-  std::unique_ptr<SegmentInfoDatabase::SegmentInfoList> get_all_segment_result_;
+  DefaultModelManager::SegmentInfoList get_all_segment_result_;
   base::WeakPtrFactory<DefaultModelManagerTest> weak_ptr_factory_{this};
 };
 
@@ -101,15 +100,15 @@
   // Verify that model exists from both sources in order: segment_1 from db,
   // segment_1 from model, segment_2 from model.
   EXPECT_EQ(3u, get_all_segment_result().size());
-  EXPECT_EQ(segment_1, get_all_segment_result()[0].first);
+  EXPECT_EQ(segment_1, get_all_segment_result()[0]->segment_info.segment_id());
   EXPECT_EQ(model_version_db,
-            get_all_segment_result()[0].second.model_version());
-  EXPECT_EQ(segment_1, get_all_segment_result()[1].first);
+            get_all_segment_result()[0]->segment_info.model_version());
+  EXPECT_EQ(segment_1, get_all_segment_result()[1]->segment_info.segment_id());
   EXPECT_EQ(model_version_default,
-            get_all_segment_result()[1].second.model_version());
-  EXPECT_EQ(segment_2, get_all_segment_result()[2].first);
+            get_all_segment_result()[1]->segment_info.model_version());
+  EXPECT_EQ(segment_2, get_all_segment_result()[2]->segment_info.segment_id());
   EXPECT_EQ(model_version_default,
-            get_all_segment_result()[2].second.model_version());
+            get_all_segment_result()[2]->segment_info.model_version());
 
   // Query again, this time with a segment ID that doesn't exist in either
   // sources.
@@ -130,7 +129,7 @@
                      weak_ptr_factory_.GetWeakPtr()));
   task_environment_.RunUntilIdle();
   EXPECT_EQ(1u, get_all_segment_result().size());
-  EXPECT_EQ(segment_2, get_all_segment_result()[0].first);
+  EXPECT_EQ(segment_2, get_all_segment_result()[0]->segment_info.segment_id());
 
   // Query for a model only available in the database.
   default_model_manager_->GetAllSegmentInfoFromBothModels(
@@ -139,7 +138,7 @@
                      weak_ptr_factory_.GetWeakPtr()));
   task_environment_.RunUntilIdle();
   EXPECT_EQ(1u, get_all_segment_result().size());
-  EXPECT_EQ(segment_3, get_all_segment_result()[0].first);
+  EXPECT_EQ(segment_3, get_all_segment_result()[0]->segment_info.segment_id());
 }
 
 }  // namespace segmentation_platform
diff --git a/components/segmentation_platform/internal/execution/model_execution_manager_impl.h b/components/segmentation_platform/internal/execution/model_execution_manager_impl.h
index 818a2fe..65b1ac1d 100644
--- a/components/segmentation_platform/internal/execution/model_execution_manager_impl.h
+++ b/components/segmentation_platform/internal/execution/model_execution_manager_impl.h
@@ -73,6 +73,7 @@
 
  private:
   friend class SegmentationPlatformServiceImplTest;
+  friend class TestServicesForPlatform;
 
   struct ExecutionState;
   struct ModelExecutionTraceEvent;
diff --git a/components/segmentation_platform/internal/segmentation_platform_service_impl.h b/components/segmentation_platform/internal/segmentation_platform_service_impl.h
index c258b26..2ad0c7e6 100644
--- a/components/segmentation_platform/internal/segmentation_platform_service_impl.h
+++ b/components/segmentation_platform/internal/segmentation_platform_service_impl.h
@@ -129,6 +129,7 @@
 
  private:
   friend class SegmentationPlatformServiceImplTest;
+  friend class TestServicesForPlatform;
 
   void OnSegmentInfoDatabaseInitialized(bool success);
   void OnSignalDatabaseInitialized(bool success);
diff --git a/components/segmentation_platform/internal/segmentation_platform_service_impl_unittest.cc b/components/segmentation_platform/internal/segmentation_platform_service_impl_unittest.cc
index 5ee42f6ab..ae8e1a7 100644
--- a/components/segmentation_platform/internal/segmentation_platform_service_impl_unittest.cc
+++ b/components/segmentation_platform/internal/segmentation_platform_service_impl_unittest.cc
@@ -9,40 +9,21 @@
 
 #include "base/bind.h"
 #include "base/files/file_path.h"
-#include "base/memory/raw_ptr.h"
 #include "base/metrics/metrics_hashes.h"
 #include "base/metrics/user_metrics.h"
 #include "base/run_loop.h"
 #include "base/test/metrics/histogram_tester.h"
-#include "base/test/simple_test_clock.h"
 #include "base/test/task_environment.h"
-#include "base/test/test_simple_task_runner.h"
-#include "components/leveldb_proto/public/proto_database_provider.h"
-#include "components/leveldb_proto/public/shared_proto_database_client_list.h"
-#include "components/leveldb_proto/testing/fake_db.h"
 #include "components/optimization_guide/machine_learning_tflite_buildflags.h"
-#include "components/prefs/pref_registry_simple.h"
 #include "components/prefs/scoped_user_pref_update.h"
-#include "components/prefs/testing_pref_service.h"
 #include "components/segmentation_platform/internal/constants.h"
-#include "components/segmentation_platform/internal/database/segment_info_database.h"
-#include "components/segmentation_platform/internal/database/signal_database_impl.h"
-#include "components/segmentation_platform/internal/database/signal_storage_config.h"
 #include "components/segmentation_platform/internal/dummy_ukm_data_manager.h"
-#include "components/segmentation_platform/internal/execution/feature_aggregator_impl.h"
-#include "components/segmentation_platform/internal/execution/mock_model_provider.h"
 #include "components/segmentation_platform/internal/proto/model_metadata.pb.h"
-#include "components/segmentation_platform/internal/proto/model_prediction.pb.h"
-#include "components/segmentation_platform/internal/proto/signal.pb.h"
-#include "components/segmentation_platform/internal/proto/signal_storage_config.pb.h"
-#include "components/segmentation_platform/internal/scheduler/model_execution_scheduler_impl.h"
-#include "components/segmentation_platform/internal/selection/segment_selector_impl.h"
+#include "components/segmentation_platform/internal/segmentation_platform_service_test_base.h"
 #include "components/segmentation_platform/internal/selection/segmentation_result_prefs.h"
-#include "components/segmentation_platform/internal/signals/histogram_signal_handler.h"
-#include "components/segmentation_platform/internal/signals/signal_filter_processor.h"
-#include "components/segmentation_platform/internal/signals/user_action_signal_handler.h"
 #include "components/segmentation_platform/internal/ukm_data_manager_impl.h"
 #include "components/segmentation_platform/public/config.h"
+#include "components/segmentation_platform/public/segment_selection_result.h"
 #include "testing/gmock/include/gmock/gmock.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
@@ -52,52 +33,10 @@
 namespace segmentation_platform {
 namespace {
 
-constexpr char kTestSegmentationKey1[] = "test_key1";
-constexpr char kTestSegmentationKey2[] = "test_key2";
-constexpr char kTestSegmentationKey3[] = "test_key3";
-
 #if BUILDFLAG(BUILD_WITH_TFLITE_LIB)
 const int64_t kModelVersion = 123;
 #endif  // BUILDFLAG(BUILD_WITH_TFLITE_LIB
 
-std::vector<std::unique_ptr<Config>> CreateTestConfigs() {
-  std::vector<std::unique_ptr<Config>> configs;
-  {
-    std::unique_ptr<Config> config = std::make_unique<Config>();
-    config->segmentation_key = kTestSegmentationKey1;
-    config->segment_selection_ttl = base::Days(28);
-    config->segment_ids = {
-        OptimizationTarget::OPTIMIZATION_TARGET_SEGMENTATION_NEW_TAB,
-        OptimizationTarget::OPTIMIZATION_TARGET_SEGMENTATION_SHARE};
-    configs.push_back(std::move(config));
-  }
-  {
-    std::unique_ptr<Config> config = std::make_unique<Config>();
-    config->segmentation_key = kTestSegmentationKey2;
-    config->segment_selection_ttl = base::Days(10);
-    config->segment_ids = {
-        OptimizationTarget::OPTIMIZATION_TARGET_SEGMENTATION_SHARE,
-        OptimizationTarget::OPTIMIZATION_TARGET_SEGMENTATION_VOICE};
-    configs.push_back(std::move(config));
-  }
-  {
-    std::unique_ptr<Config> config = std::make_unique<Config>();
-    config->segmentation_key = kTestSegmentationKey3;
-    config->segment_selection_ttl = base::Days(14);
-    config->segment_ids = {
-        OptimizationTarget::OPTIMIZATION_TARGET_SEGMENTATION_NEW_TAB};
-    configs.push_back(std::move(config));
-  }
-  {
-    // Empty config.
-    std::unique_ptr<Config> config = std::make_unique<Config>();
-    config->segmentation_key = "test_key";
-    configs.push_back(std::move(config));
-  }
-
-  return configs;
-}
-
 // A mock of the ServiceProxy::Observer.
 class MockServiceProxyObserver : public ServiceProxy::Observer {
  public:
@@ -113,73 +52,32 @@
 
 }  // namespace
 
-class SegmentationPlatformServiceImplTest : public testing::Test {
+class SegmentationPlatformServiceImplTest
+    : public testing::Test,
+      public SegmentationPlatformServiceTestBase {
  public:
   explicit SegmentationPlatformServiceImplTest(
-      std::unique_ptr<UkmDataManager> ukm_data_manager = nullptr) {
-    if (ukm_data_manager) {
-      ukm_data_manager_ = std::move(ukm_data_manager);
-    } else {
-      ukm_data_manager_ = std::make_unique<UkmDataManagerImpl>();
-    }
-  }
+      std::unique_ptr<UkmDataManager> ukm_data_manager = nullptr)
+      : ukm_data_manager_(ukm_data_manager
+                              ? std::move(ukm_data_manager)
+                              : std::make_unique<UkmDataManagerImpl>()) {}
 
   ~SegmentationPlatformServiceImplTest() override = default;
 
   void SetUp() override {
-    task_runner_ = base::MakeRefCounted<base::TestSimpleTaskRunner>();
     base::SetRecordActionTaskRunner(
         task_environment_.GetMainThreadTaskRunner());
 
-    auto segment_db =
-        std::make_unique<leveldb_proto::test::FakeDB<proto::SegmentInfo>>(
-            &segment_db_entries_);
-    auto signal_db =
-        std::make_unique<leveldb_proto::test::FakeDB<proto::SignalData>>(
-            &signal_db_entries_);
-    auto segment_storage_config_db = std::make_unique<
-        leveldb_proto::test::FakeDB<proto::SignalStorageConfigs>>(
-        &segment_storage_config_db_entries_);
-    segment_db_ = segment_db.get();
-    signal_db_ = signal_db.get();
-    segment_storage_config_db_ = segment_storage_config_db.get();
-
-    SegmentationPlatformService::RegisterProfilePrefs(pref_service_.registry());
-    SetUpPrefs();
-
-    std::vector<std::unique_ptr<Config>> configs = CreateTestConfigs();
     // TODO(ssid): use mock a history service here.
-    segmentation_platform_service_impl_ =
-        std::make_unique<SegmentationPlatformServiceImpl>(
-            std::move(segment_db), std::move(signal_db),
-            std::move(segment_storage_config_db), ukm_data_manager_.get(),
-            std::make_unique<TestModelProviderFactory>(&model_provider_data_),
-            &pref_service_, /*history_service=*/nullptr, task_runner_,
-            &test_clock_, std::move(configs));
+    SegmentationPlatformServiceTestBase::InitPlatform(
+        ukm_data_manager_.get(), /*history_service=*/nullptr);
+
     segmentation_platform_service_impl_->GetServiceProxy()->AddObserver(
         &observer_);
   }
 
   void TearDown() override {
-    segmentation_platform_service_impl_.reset();
-    // Allow for the SegmentationModelExecutor owned by SegmentationModelHandler
-    // to be destroyed.
-    task_runner_->RunUntilIdle();
-  }
-
-  virtual void SetUpPrefs() {
-    DictionaryPrefUpdate update(&pref_service_, kSegmentationResultPref);
-    base::Value* dictionary = update.Get();
-
-    base::Value segmentation_result(base::Value::Type::DICTIONARY);
-    segmentation_result.SetIntKey(
-        "segment_id",
-        OptimizationTarget::OPTIMIZATION_TARGET_SEGMENTATION_SHARE);
-    dictionary->SetKey(kTestSegmentationKey1, std::move(segmentation_result));
-  }
-
-  virtual std::vector<std::unique_ptr<Config>> CreateConfigs() {
-    return CreateTestConfigs();
+    SegmentationPlatformServiceTestBase::DestroyPlatform();
   }
 
   void OnGetSelectedSegment(base::RepeatingClosure closure,
@@ -343,22 +241,8 @@
 
   base::test::TaskEnvironment task_environment_{
       base::test::TaskEnvironment::TimeSource::MOCK_TIME};
-  scoped_refptr<base::TestSimpleTaskRunner> task_runner_;
-  std::map<std::string, proto::SegmentInfo> segment_db_entries_;
-  std::map<std::string, proto::SignalData> signal_db_entries_;
-  std::map<std::string, proto::SignalStorageConfigs>
-      segment_storage_config_db_entries_;
-  raw_ptr<leveldb_proto::test::FakeDB<proto::SegmentInfo>> segment_db_;
-  raw_ptr<leveldb_proto::test::FakeDB<proto::SignalData>> signal_db_;
-  raw_ptr<leveldb_proto::test::FakeDB<proto::SignalStorageConfigs>>
-      segment_storage_config_db_;
-  TestModelProviderFactory::Data model_provider_data_;
-  TestingPrefServiceSimple pref_service_;
-  base::SimpleTestClock test_clock_;
-  std::unique_ptr<UkmDataManager> ukm_data_manager_;
-  std::unique_ptr<SegmentationPlatformServiceImpl>
-      segmentation_platform_service_impl_;
   MockServiceProxyObserver observer_;
+  std::unique_ptr<UkmDataManager> ukm_data_manager_;
 };
 
 TEST_F(SegmentationPlatformServiceImplTest, InitializationFlow) {
diff --git a/components/segmentation_platform/internal/segmentation_platform_service_test_base.cc b/components/segmentation_platform/internal/segmentation_platform_service_test_base.cc
new file mode 100644
index 0000000..d488984
--- /dev/null
+++ b/components/segmentation_platform/internal/segmentation_platform_service_test_base.cc
@@ -0,0 +1,124 @@
+// 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 "components/segmentation_platform/internal/segmentation_platform_service_test_base.h"
+
+#include "base/test/test_simple_task_runner.h"
+#include "base/time/time.h"
+#include "base/values.h"
+#include "components/prefs/scoped_user_pref_update.h"
+#include "components/segmentation_platform/internal/constants.h"
+#include "components/segmentation_platform/internal/database/segment_info_database.h"
+#include "components/segmentation_platform/internal/execution/mock_model_provider.h"
+#include "components/segmentation_platform/internal/segmentation_platform_service_impl.h"
+#include "components/segmentation_platform/internal/ukm_data_manager.h"
+#include "components/segmentation_platform/public/config.h"
+
+namespace segmentation_platform {
+
+namespace {
+
+std::vector<std::unique_ptr<Config>> CreateTestConfigs() {
+  std::vector<std::unique_ptr<Config>> configs;
+  {
+    std::unique_ptr<Config> config = std::make_unique<Config>();
+    config->segmentation_key = kTestSegmentationKey1;
+    config->segment_selection_ttl = base::Days(28);
+    config->segment_ids = {
+        OptimizationTarget::OPTIMIZATION_TARGET_SEGMENTATION_NEW_TAB,
+        OptimizationTarget::OPTIMIZATION_TARGET_SEGMENTATION_SHARE};
+    configs.push_back(std::move(config));
+  }
+  {
+    std::unique_ptr<Config> config = std::make_unique<Config>();
+    config->segmentation_key = kTestSegmentationKey2;
+    config->segment_selection_ttl = base::Days(10);
+    config->segment_ids = {
+        OptimizationTarget::OPTIMIZATION_TARGET_SEGMENTATION_SHARE,
+        OptimizationTarget::OPTIMIZATION_TARGET_SEGMENTATION_VOICE};
+    configs.push_back(std::move(config));
+  }
+  {
+    std::unique_ptr<Config> config = std::make_unique<Config>();
+    config->segmentation_key = kTestSegmentationKey3;
+    config->segment_selection_ttl = base::Days(14);
+    config->segment_ids = {
+        OptimizationTarget::OPTIMIZATION_TARGET_SEGMENTATION_NEW_TAB};
+    configs.push_back(std::move(config));
+  }
+  {
+    // Empty config.
+    std::unique_ptr<Config> config = std::make_unique<Config>();
+    config->segmentation_key = "test_key";
+    configs.push_back(std::move(config));
+  }
+
+  return configs;
+}
+
+}  // namespace
+
+constexpr char kTestSegmentationKey1[] = "test_key1";
+constexpr char kTestSegmentationKey2[] = "test_key2";
+constexpr char kTestSegmentationKey3[] = "test_key3";
+
+SegmentationPlatformServiceTestBase::SegmentationPlatformServiceTestBase() =
+    default;
+SegmentationPlatformServiceTestBase::~SegmentationPlatformServiceTestBase() =
+    default;
+
+void SegmentationPlatformServiceTestBase::InitPlatform(
+    UkmDataManager* ukm_data_manager,
+    history::HistoryService* history_service) {
+  task_runner_ = base::MakeRefCounted<base::TestSimpleTaskRunner>();
+
+  auto segment_db =
+      std::make_unique<leveldb_proto::test::FakeDB<proto::SegmentInfo>>(
+          &segment_db_entries_);
+  auto signal_db =
+      std::make_unique<leveldb_proto::test::FakeDB<proto::SignalData>>(
+          &signal_db_entries_);
+  auto segment_storage_config_db = std::make_unique<
+      leveldb_proto::test::FakeDB<proto::SignalStorageConfigs>>(
+      &segment_storage_config_db_entries_);
+  segment_db_ = segment_db.get();
+  signal_db_ = signal_db.get();
+  segment_storage_config_db_ = segment_storage_config_db.get();
+
+  SegmentationPlatformService::RegisterProfilePrefs(pref_service_.registry());
+  SetUpPrefs();
+
+  std::vector<std::unique_ptr<Config>> configs = CreateTestConfigs();
+  segmentation_platform_service_impl_ =
+      std::make_unique<SegmentationPlatformServiceImpl>(
+          std::move(segment_db), std::move(signal_db),
+          std::move(segment_storage_config_db), ukm_data_manager,
+          std::make_unique<TestModelProviderFactory>(&model_provider_data_),
+          &pref_service_, history_service, task_runner_, &test_clock_,
+          std::move(configs));
+}
+
+void SegmentationPlatformServiceTestBase::DestroyPlatform() {
+  segmentation_platform_service_impl_.reset();
+  // Allow for the SegmentationModelExecutor owned by SegmentationModelHandler
+  // to be destroyed.
+  task_runner_->RunUntilIdle();
+}
+
+void SegmentationPlatformServiceTestBase::SetUpPrefs() {
+  DictionaryPrefUpdate update(&pref_service_, kSegmentationResultPref);
+  base::Value* dictionary = update.Get();
+
+  base::Value segmentation_result(base::Value::Type::DICTIONARY);
+  segmentation_result.SetIntKey(
+      "segment_id", OptimizationTarget::OPTIMIZATION_TARGET_SEGMENTATION_SHARE);
+  dictionary->SetKey(kTestSegmentationKey1, std::move(segmentation_result));
+}
+
+std::vector<std::unique_ptr<Config>>
+SegmentationPlatformServiceTestBase::CreateConfigs() {
+  return CreateTestConfigs();
+}
+
+}  // namespace segmentation_platform
diff --git a/components/segmentation_platform/internal/segmentation_platform_service_test_base.h b/components/segmentation_platform/internal/segmentation_platform_service_test_base.h
new file mode 100644
index 0000000..4e896a1c
--- /dev/null
+++ b/components/segmentation_platform/internal/segmentation_platform_service_test_base.h
@@ -0,0 +1,77 @@
+// 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 COMPONENTS_SEGMENTATION_PLATFORM_INTERNAL_SEGMENTATION_PLATFORM_SERVICE_TEST_BASE_H_
+#define COMPONENTS_SEGMENTATION_PLATFORM_INTERNAL_SEGMENTATION_PLATFORM_SERVICE_TEST_BASE_H_
+
+#include <memory>
+#include <vector>
+
+#include "base/test/simple_test_clock.h"
+#include "components/leveldb_proto/testing/fake_db.h"
+#include "components/prefs/testing_pref_service.h"
+#include "components/segmentation_platform/internal/execution/mock_model_provider.h"
+#include "components/segmentation_platform/internal/proto/model_prediction.pb.h"
+#include "components/segmentation_platform/internal/proto/signal.pb.h"
+#include "components/segmentation_platform/internal/proto/signal_storage_config.pb.h"
+
+namespace history {
+class HistoryService;
+}
+
+namespace segmentation_platform {
+
+struct Config;
+class SegmentationPlatformServiceImpl;
+class UkmDataManager;
+
+extern const char kTestSegmentationKey1[];
+extern const char kTestSegmentationKey2[];
+extern const char kTestSegmentationKey3[];
+
+// Wrapper around SegmentationPlatformServiceImpl for testing. Holds and manages
+// a single platform instance.
+class SegmentationPlatformServiceTestBase {
+ public:
+  SegmentationPlatformServiceTestBase();
+  virtual ~SegmentationPlatformServiceTestBase();
+
+  // Creates the platform service, does not wait for initialization to complete.
+  void InitPlatform(UkmDataManager* ukm_data_manager,
+                    history::HistoryService* history_service);
+
+  // Destroys the platform, and setup.
+  void DestroyPlatform();
+
+  // Called to register additional segmentation prefs before creating the
+  // platform.
+  virtual void SetUpPrefs();
+  // Called to create a config before creating the platform. Uses a default
+  // config with 3 keys: kTestSegmentationKey* with different selection TTLs.
+  virtual std::vector<std::unique_ptr<Config>> CreateConfigs();
+
+  leveldb_proto::test::FakeDB<proto::SegmentInfo>& segment_db() {
+    return *segment_db_;
+  }
+
+ protected:
+  scoped_refptr<base::TestSimpleTaskRunner> task_runner_;
+  std::map<std::string, proto::SegmentInfo> segment_db_entries_;
+  std::map<std::string, proto::SignalData> signal_db_entries_;
+  std::map<std::string, proto::SignalStorageConfigs>
+      segment_storage_config_db_entries_;
+  raw_ptr<leveldb_proto::test::FakeDB<proto::SegmentInfo>> segment_db_;
+  raw_ptr<leveldb_proto::test::FakeDB<proto::SignalData>> signal_db_;
+  raw_ptr<leveldb_proto::test::FakeDB<proto::SignalStorageConfigs>>
+      segment_storage_config_db_;
+  TestModelProviderFactory::Data model_provider_data_;
+  TestingPrefServiceSimple pref_service_;
+  base::SimpleTestClock test_clock_;
+  std::unique_ptr<SegmentationPlatformServiceImpl>
+      segmentation_platform_service_impl_;
+};
+
+}  // namespace segmentation_platform
+
+#endif  // COMPONENTS_SEGMENTATION_PLATFORM_INTERNAL_SEGMENTATION_PLATFORM_SERVICE_TEST_BASE_H_
diff --git a/components/segmentation_platform/internal/selection/segment_result_provider.cc b/components/segmentation_platform/internal/selection/segment_result_provider.cc
index 93bcc4cc..3fd85094 100644
--- a/components/segmentation_platform/internal/selection/segment_result_provider.cc
+++ b/components/segmentation_platform/internal/selection/segment_result_provider.cc
@@ -65,18 +65,17 @@
     std::string segmentation_key;
   };
 
-  void OnGetSegmentInfo(std::unique_ptr<RequestState> request_state,
-                        absl::optional<proto::SegmentInfo> available_segment);
+  void OnGetSegmentInfo(
+      std::unique_ptr<RequestState> request_state,
+      DefaultModelManager::SegmentInfoList available_segments);
 
   void TryGetScoreFromDefaultModel(
       std::unique_ptr<RequestState> request_state,
-      SegmentResultProvider::ResultState existing_state);
-  void OnDefaultModelFetched(
-      std::unique_ptr<RequestState> request_state,
-      std::unique_ptr<DefaultModelManager::SegmentInfoList> metadata_list);
+      SegmentResultProvider::ResultState existing_state,
+      DefaultModelManager::SegmentInfoList available_segments);
   void OnDefaultModelExecuted(
       std::unique_ptr<RequestState> request_state,
-      proto::SegmentInfo segment_info,
+      std::unique_ptr<proto::SegmentInfo> segment_info,
       const std::pair<float, ModelExecutionStatus>& result);
 
   void PostResultCallback(std::unique_ptr<RequestState> request_state,
@@ -107,54 +106,64 @@
       default_model_manager_
           ? default_model_manager_->GetDefaultProvider(segment_id)
           : nullptr;
-  // TODO(ssid): Change default model manager to return both info instead of
-  // requesting here.
-  segment_database_->GetSegmentInfo(
-      segment_id,
+
+  default_model_manager_->GetAllSegmentInfoFromBothModels(
+      {segment_id}, segment_database_,
       base::BindOnce(&SegmentResultProviderImpl::OnGetSegmentInfo,
                      weak_ptr_factory_.GetWeakPtr(), std::move(request_state)));
 }
 
 void SegmentResultProviderImpl::OnGetSegmentInfo(
     std::unique_ptr<RequestState> request_state,
-    absl::optional<proto::SegmentInfo> available_segment) {
+    DefaultModelManager::SegmentInfoList available_segments) {
+  const proto::SegmentInfo* db_segment_info = nullptr;
+  for (const auto& info : available_segments) {
+    DCHECK_EQ(request_state->segment_id, info->segment_info.segment_id());
+    if (info->segment_source == DefaultModelManager::SegmentSource::DATABASE) {
+      db_segment_info = &info->segment_info;
+      break;
+    }
+  }
+
   // Don't compute results if we don't have enough signals, or don't have
   // valid unexpired results for any of the segments.
-  if (!available_segment) {
+  if (!db_segment_info) {
     VLOG(1) << __func__ << ": segment="
             << OptimizationTarget_Name(request_state->segment_id)
             << " does not have segment info.";
     TryGetScoreFromDefaultModel(std::move(request_state),
-                                ResultState::kSegmentNotAvailable);
+                                ResultState::kSegmentNotAvailable,
+                                std::move(available_segments));
     return;
   }
 
-  proto::SegmentInfo& segment_info = *available_segment;
   // TODO(ssid): Remove this check since scheduler does this before executing
   // the model.
   if (!force_refresh_results_ &&
       !signal_storage_config_->MeetsSignalCollectionRequirement(
-          segment_info.model_metadata())) {
+          db_segment_info->model_metadata())) {
     VLOG(1) << __func__ << ": segment="
-            << OptimizationTarget_Name(segment_info.segment_id())
+            << OptimizationTarget_Name(db_segment_info->segment_id())
             << " does not meet signal collection requirements.";
     TryGetScoreFromDefaultModel(std::move(request_state),
-                                ResultState::kSignalsNotCollected);
+                                ResultState::kSignalsNotCollected,
+                                std::move(available_segments));
     return;
   }
 
-  if (metadata_utils::HasExpiredOrUnavailableResult(segment_info,
+  if (metadata_utils::HasExpiredOrUnavailableResult(*db_segment_info,
                                                     clock_->Now())) {
     VLOG(1) << __func__ << ": segment="
-            << OptimizationTarget_Name(segment_info.segment_id())
+            << OptimizationTarget_Name(db_segment_info->segment_id())
             << " has expired or unavailable result.";
     TryGetScoreFromDefaultModel(std::move(request_state),
-                                ResultState::kDatabaseScoreNotReady);
+                                ResultState::kDatabaseScoreNotReady,
+                                std::move(available_segments));
     return;
   }
 
   int rank =
-      ComputeDiscreteMapping(request_state->segmentation_key, segment_info);
+      ComputeDiscreteMapping(request_state->segmentation_key, *db_segment_info);
   PostResultCallback(
       std::move(request_state),
       std::make_unique<SegmentResult>(ResultState::kSuccessFromDatabase, rank));
@@ -162,7 +171,8 @@
 
 void SegmentResultProviderImpl::TryGetScoreFromDefaultModel(
     std::unique_ptr<RequestState> request_state,
-    SegmentResultProvider::ResultState existing_state) {
+    SegmentResultProvider::ResultState existing_state,
+    DefaultModelManager::SegmentInfoList available_segments) {
   if (!request_state->default_provider ||
       !request_state->default_provider->ModelAvailable()) {
     PostResultCallback(std::move(request_state),
@@ -170,29 +180,29 @@
     return;
   }
 
-  OptimizationTarget segment_id = request_state->segment_id;
-  default_model_manager_->GetAllSegmentInfoFromDefaultModel(
-      {segment_id},
-      base::BindOnce(&SegmentResultProviderImpl::OnDefaultModelFetched,
-                     weak_ptr_factory_.GetWeakPtr(), std::move(request_state)));
-}
+  std::unique_ptr<proto::SegmentInfo> default_segment_info;
+  for (auto& info : available_segments) {
+    DCHECK_EQ(request_state->segment_id, info->segment_info.segment_id());
+    if (info->segment_source ==
+        DefaultModelManager::SegmentSource::DEFAULT_MODEL) {
+      default_segment_info = std::make_unique<proto::SegmentInfo>();
+      default_segment_info->Swap(&info->segment_info);
+      break;
+    }
+  }
 
-void SegmentResultProviderImpl::OnDefaultModelFetched(
-    std::unique_ptr<RequestState> request_state,
-    std::unique_ptr<DefaultModelManager::SegmentInfoList> metadata_list) {
-  if (!metadata_list || metadata_list->size() != 1 ||
-      metadata_list->back().first != request_state->segment_id) {
+  if (!default_segment_info) {
     PostResultCallback(std::move(request_state),
                        std::make_unique<SegmentResult>(
                            ResultState::kDefaultModelMetadataMissing));
     return;
   }
 
-  proto::SegmentInfo& segment_info = (*metadata_list)[0].second;
-  DCHECK_EQ(metadata_utils::ValidationResult::kValidationSuccess,
-            metadata_utils::ValidateMetadata(segment_info.model_metadata()));
+  DCHECK_EQ(
+      metadata_utils::ValidationResult::kValidationSuccess,
+      metadata_utils::ValidateMetadata(default_segment_info->model_metadata()));
   if (!signal_storage_config_->MeetsSignalCollectionRequirement(
-          segment_info.model_metadata())) {
+          default_segment_info->model_metadata())) {
     PostResultCallback(std::move(request_state),
                        std::make_unique<SegmentResult>(
                            ResultState::kDefaultModelSignalNotCollected));
@@ -201,21 +211,23 @@
 
   ModelProvider* default_provider = request_state->default_provider;
   DCHECK(default_provider);
+  // The reference is kept alive by the unique_ptr in the callback.
+  const proto::SegmentInfo& info_ref = *default_segment_info;
   execution_manager_->ExecuteModel(
-      segment_info, default_provider,
+      info_ref, default_provider,
       base::BindOnce(&SegmentResultProviderImpl::OnDefaultModelExecuted,
                      weak_ptr_factory_.GetWeakPtr(), std::move(request_state),
-                     segment_info));
+                     std::move(default_segment_info)));
 }
 
 void SegmentResultProviderImpl::OnDefaultModelExecuted(
     std::unique_ptr<RequestState> request_state,
-    proto::SegmentInfo segment_info,
+    std::unique_ptr<proto::SegmentInfo> segment_info,
     const std::pair<float, ModelExecutionStatus>& result) {
   if (result.second == ModelExecutionStatus::kSuccess) {
-    segment_info.mutable_prediction_result()->set_result(result.first);
+    segment_info->mutable_prediction_result()->set_result(result.first);
     int rank =
-        ComputeDiscreteMapping(request_state->segmentation_key, segment_info);
+        ComputeDiscreteMapping(request_state->segmentation_key, *segment_info);
     PostResultCallback(std::move(request_state),
                        std::make_unique<SegmentResult>(
                            ResultState::kDefaultModelScoreUsed, rank));
diff --git a/components/segmentation_platform/internal/selection/segment_selector_unittest.cc b/components/segmentation_platform/internal/selection/segment_selector_unittest.cc
index f2b8b35..5c015e51 100644
--- a/components/segmentation_platform/internal/selection/segment_selector_unittest.cc
+++ b/components/segmentation_platform/internal/selection/segment_selector_unittest.cc
@@ -307,7 +307,8 @@
   // Construct a segment selector. It should read result from last session.
   segment_selector_ = std::make_unique<SegmentSelectorImpl>(
       segment_database_.get(), &signal_storage_config_, prefs_.get(), &config_,
-      &clock_, PlatformOptions::CreateDefault(), nullptr, nullptr);
+      &clock_, PlatformOptions::CreateDefault(), default_manager_.get(),
+      nullptr);
 
   SegmentSelectionResult result;
   result.segment = segment_id0;
diff --git a/components/segmentation_platform/internal/signals/signal_filter_processor.cc b/components/segmentation_platform/internal/signals/signal_filter_processor.cc
index 4b91710..808f09e 100644
--- a/components/segmentation_platform/internal/signals/signal_filter_processor.cc
+++ b/components/segmentation_platform/internal/signals/signal_filter_processor.cc
@@ -24,10 +24,9 @@
 class FilterExtractor {
  public:
   explicit FilterExtractor(
-      const std::vector<std::pair<OptimizationTarget, proto::SegmentInfo>>&
-          segment_infos) {
-    for (const auto& pair : segment_infos) {
-      const proto::SegmentInfo& segment_info = pair.second;
+      const DefaultModelManager::SegmentInfoList& segment_infos) {
+    for (const auto& info : segment_infos) {
+      const proto::SegmentInfo& segment_info = info->segment_info;
       const auto& metadata = segment_info.model_metadata();
       AddUmaFeatures(metadata);
       AddUkmFeatures(metadata);
@@ -105,8 +104,8 @@
 }
 
 void SignalFilterProcessor::FilterSignals(
-    std::unique_ptr<SegmentInfoDatabase::SegmentInfoList> segment_infos) {
-  FilterExtractor extractor(*segment_infos);
+    DefaultModelManager::SegmentInfoList segment_infos) {
+  FilterExtractor extractor(segment_infos);
 
   stats::RecordSignalsListeningCount(extractor.user_actions,
                                      extractor.histograms);
diff --git a/components/segmentation_platform/internal/signals/signal_filter_processor.h b/components/segmentation_platform/internal/signals/signal_filter_processor.h
index e75a84e7..1465966 100644
--- a/components/segmentation_platform/internal/signals/signal_filter_processor.h
+++ b/components/segmentation_platform/internal/signals/signal_filter_processor.h
@@ -8,14 +8,14 @@
 #include "base/memory/raw_ptr.h"
 #include "base/memory/weak_ptr.h"
 #include "components/optimization_guide/proto/models.pb.h"
-#include "components/segmentation_platform/internal/database/segment_info_database.h"
+#include "components/segmentation_platform/internal/execution/default_model_manager.h"
 
 using optimization_guide::proto::OptimizationTarget;
 
 namespace segmentation_platform {
 
-class DefaultModelManager;
 class HistogramSignalHandler;
+class SegmentInfoDatabase;
 class UserActionSignalHandler;
 class UkmDataManager;
 
@@ -48,8 +48,7 @@
   void EnableMetrics(bool enable_metrics);
 
  private:
-  void FilterSignals(
-      std::unique_ptr<SegmentInfoDatabase::SegmentInfoList> segment_infos);
+  void FilterSignals(DefaultModelManager::SegmentInfoList segment_infos);
 
   raw_ptr<SegmentInfoDatabase> segment_database_;
   raw_ptr<UserActionSignalHandler> user_action_signal_handler_;
diff --git a/components/segmentation_platform/internal/signals/signal_filter_processor_unittest.cc b/components/segmentation_platform/internal/signals/signal_filter_processor_unittest.cc
index 77e935ec..dfc35a90 100644
--- a/components/segmentation_platform/internal/signals/signal_filter_processor_unittest.cc
+++ b/components/segmentation_platform/internal/signals/signal_filter_processor_unittest.cc
@@ -56,18 +56,30 @@
       const std::vector<OptimizationTarget>& segment_ids,
       MultipleSegmentInfoCallback callback) override {
     base::ThreadTaskRunnerHandle::Get()->PostTask(
-        FROM_HERE,
-        base::BindOnce(
-            std::move(callback),
-            std::make_unique<DefaultModelManager::SegmentInfoList>()));
+        FROM_HERE, base::BindOnce(std::move(callback),
+                                  DefaultModelManager::SegmentInfoList()));
   }
 
   void GetAllSegmentInfoFromBothModels(
       const std::vector<OptimizationTarget>& segment_ids,
       SegmentInfoDatabase* segment_database,
       MultipleSegmentInfoCallback callback) override {
-    segment_database->GetSegmentInfoForSegments(segment_ids,
-                                                std::move(callback));
+    segment_database->GetSegmentInfoForSegments(
+        segment_ids,
+        base::BindOnce(
+            [](DefaultModelManager::MultipleSegmentInfoCallback callback,
+               std::unique_ptr<SegmentInfoDatabase::SegmentInfoList> db_list) {
+              DefaultModelManager::SegmentInfoList list;
+              for (auto& pair : *db_list) {
+                list.push_back(std::make_unique<
+                               DefaultModelManager::SegmentInfoWrapper>());
+                list.back()->segment_source =
+                    DefaultModelManager::SegmentSource::DATABASE;
+                list.back()->segment_info.Swap(&pair.second);
+              }
+              std::move(callback).Run(std::move(list));
+            },
+            std::move(callback)));
   }
 };
 
diff --git a/components/segmentation_platform/internal/signals/user_action_signal_handler.cc b/components/segmentation_platform/internal/signals/user_action_signal_handler.cc
index af8195f..b97d58d 100644
--- a/components/segmentation_platform/internal/signals/user_action_signal_handler.cc
+++ b/components/segmentation_platform/internal/signals/user_action_signal_handler.cc
@@ -19,7 +19,8 @@
 }
 
 UserActionSignalHandler::~UserActionSignalHandler() {
-  base::RemoveActionCallback(action_callback_);
+  if (metrics_enabled_)
+    base::RemoveActionCallback(action_callback_);
 }
 
 void UserActionSignalHandler::EnableMetrics(bool enable_metrics) {
diff --git a/components/segmentation_platform/internal/ukm_data_manager_impl.cc b/components/segmentation_platform/internal/ukm_data_manager_impl.cc
index fe2333e5a..b26fb22 100644
--- a/components/segmentation_platform/internal/ukm_data_manager_impl.cc
+++ b/components/segmentation_platform/internal/ukm_data_manager_impl.cc
@@ -24,8 +24,16 @@
   ukm_database_.reset();
 }
 
+void UkmDataManagerImpl::InitializeForTesting(
+    std::unique_ptr<UkmDatabase> ukm_database) {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_check_);
+  DCHECK(!ukm_database_);
+  ukm_database_ = std::move(ukm_database);
+}
+
 void UkmDataManagerImpl::Initialize(const base::FilePath& database_path) {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_check_);
+  DCHECK(!ukm_database_);
   ukm_database_ = std::make_unique<UkmDatabase>(database_path);
 }
 
@@ -74,6 +82,9 @@
 
 void UkmDataManagerImpl::StopObservingUkm() {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_check_);
+  if (!ukm_observer_)
+    return;
+
   DCHECK(ukm_database_);
   DCHECK(url_signal_handler_);
   ukm_observer_.reset();
diff --git a/components/segmentation_platform/internal/ukm_data_manager_impl.h b/components/segmentation_platform/internal/ukm_data_manager_impl.h
index fec82d8e..52e93cf6 100644
--- a/components/segmentation_platform/internal/ukm_data_manager_impl.h
+++ b/components/segmentation_platform/internal/ukm_data_manager_impl.h
@@ -24,6 +24,8 @@
   UkmDataManagerImpl(UkmDataManagerImpl&) = delete;
   UkmDataManagerImpl& operator=(UkmDataManagerImpl&) = delete;
 
+  void InitializeForTesting(std::unique_ptr<UkmDatabase> ukm_database);
+
   // UkmDataManager implementation:
   void Initialize(const base::FilePath& database_path) override;
   bool IsUkmEngineEnabled() override;
diff --git a/components/segmentation_platform/internal/ukm_data_manager_impl_unittest.cc b/components/segmentation_platform/internal/ukm_data_manager_impl_unittest.cc
new file mode 100644
index 0000000..18497dd4
--- /dev/null
+++ b/components/segmentation_platform/internal/ukm_data_manager_impl_unittest.cc
@@ -0,0 +1,401 @@
+// 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 "components/segmentation_platform/internal/ukm_data_manager_impl.h"
+
+#include "base/files/scoped_temp_dir.h"
+#include "base/metrics/metrics_hashes.h"
+#include "base/test/task_environment.h"
+#include "components/history/core/browser/history_service.h"
+#include "components/history/core/test/history_service_test_util.h"
+#include "components/segmentation_platform/internal/database/mock_ukm_database.h"
+#include "components/segmentation_platform/internal/database/ukm_types.h"
+#include "components/segmentation_platform/internal/execution/model_execution_manager_impl.h"
+#include "components/segmentation_platform/internal/segmentation_platform_service_impl.h"
+#include "components/segmentation_platform/internal/segmentation_platform_service_test_base.h"
+#include "components/ukm/test_ukm_recorder.h"
+#include "services/metrics/public/cpp/ukm_builders.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace segmentation_platform {
+
+namespace {
+
+using testing::_;
+using ukm::builders::PageLoad;
+using ukm::builders::PaintPreviewCapture;
+
+constexpr ukm::SourceId kSourceId = 10;
+constexpr ukm::SourceId kSourceId2 = 12;
+
+ukm::mojom::UkmEntryPtr GetSamplePageLoadEntry(
+    ukm::SourceId source_id = kSourceId) {
+  ukm::mojom::UkmEntryPtr entry = ukm::mojom::UkmEntry::New();
+  entry->source_id = source_id;
+  entry->event_hash = PageLoad::kEntryNameHash;
+  entry->metrics[PageLoad::kCpuTimeNameHash] = 10;
+  entry->metrics[PageLoad::kIsNewBookmarkNameHash] = 20;
+  entry->metrics[PageLoad::kIsNTPCustomLinkNameHash] = 30;
+  return entry;
+}
+
+ukm::mojom::UkmEntryPtr GetSamplePaintPreviewEntry(
+    ukm::SourceId source_id = kSourceId) {
+  ukm::mojom::UkmEntryPtr entry = ukm::mojom::UkmEntry::New();
+  entry->source_id = source_id;
+  entry->event_hash = PaintPreviewCapture::kEntryNameHash;
+  entry->metrics[PaintPreviewCapture::kBlinkCaptureTimeNameHash] = 5;
+  entry->metrics[PaintPreviewCapture::kCompressedOnDiskSizeNameHash] = 15;
+  return entry;
+}
+
+proto::SegmentationModelMetadata PageLoadModelMetadata() {
+  proto::SegmentationModelMetadata metadata;
+  metadata.set_time_unit(proto::TimeUnit::DAY);
+  metadata.set_bucket_duration(42u);
+  auto* feature = metadata.add_input_features();
+  auto* sql_feature = feature->mutable_sql_feature();
+  sql_feature->set_sql("SELECT COUNT(*) from metrics;");
+
+  auto* ukm_event = sql_feature->mutable_signal_filter()->add_ukm_events();
+  ukm_event->set_event_hash(PageLoad::kEntryNameHash);
+  ukm_event->add_metric_hash_filter(PageLoad::kCpuTimeNameHash);
+  ukm_event->add_metric_hash_filter(PageLoad::kIsNewBookmarkNameHash);
+  return metadata;
+}
+
+proto::SegmentationModelMetadata PaintPreviewModelMetadata() {
+  proto::SegmentationModelMetadata metadata;
+  metadata.set_time_unit(proto::TimeUnit::DAY);
+  metadata.set_bucket_duration(42u);
+
+  auto* feature = metadata.add_input_features();
+  auto* sql_feature = feature->mutable_sql_feature();
+  sql_feature->set_sql("SELECT COUNT(*) from metrics;");
+  auto* ukm_event2 = sql_feature->mutable_signal_filter()->add_ukm_events();
+  ukm_event2->set_event_hash(PaintPreviewCapture::kEntryNameHash);
+  ukm_event2->add_metric_hash_filter(
+      PaintPreviewCapture::kBlinkCaptureTimeNameHash);
+  return metadata;
+}
+
+}  // namespace
+
+class TestServicesForPlatform : public SegmentationPlatformServiceTestBase {
+ public:
+  explicit TestServicesForPlatform(UkmDataManagerImpl* ukm_data_manager) {
+    EXPECT_TRUE(profile_dir.CreateUniqueTempDir());
+    history_service = history::CreateHistoryService(profile_dir.GetPath(),
+                                                    /*create_db=*/true);
+
+    InitPlatform(ukm_data_manager, history_service.get());
+
+    segment_db_->InitStatusCallback(leveldb_proto::Enums::InitStatus::kOK);
+    signal_db_->InitStatusCallback(leveldb_proto::Enums::InitStatus::kOK);
+    segment_storage_config_db_->InitStatusCallback(
+        leveldb_proto::Enums::InitStatus::kOK);
+    segment_storage_config_db_->LoadCallback(true);
+
+    // If initialization is succeeded, model execution scheduler should start
+    // querying segment db.
+    segment_db_->LoadCallback(true);
+  }
+
+  ~TestServicesForPlatform() override {
+    DestroyPlatform();
+    history_service.reset();
+  }
+
+  void AddModel(const proto::SegmentationModelMetadata& metadata) {
+    ModelExecutionManagerImpl* mem_impl =
+        static_cast<ModelExecutionManagerImpl*>(
+            segmentation_platform_service_impl_->model_execution_manager_
+                .get());
+    mem_impl->OnSegmentationModelUpdated(
+        OptimizationTarget::OPTIMIZATION_TARGET_SEGMENTATION_SHARE, metadata,
+        0);
+    segment_db_->GetCallback(true);
+    segment_db_->UpdateCallback(true);
+    segment_db_->LoadCallback(true);
+    base::RunLoop().RunUntilIdle();
+  }
+
+  SegmentationPlatformServiceImpl& platform() {
+    return *segmentation_platform_service_impl_;
+  }
+
+  base::ScopedTempDir profile_dir;
+  std::unique_ptr<history::HistoryService> history_service;
+};
+
+class UkmDataManagerImplTest : public testing::Test {
+ public:
+  UkmDataManagerImplTest() = default;
+  ~UkmDataManagerImplTest() override = default;
+
+  void SetUp() override {
+    data_manager_ = std::make_unique<UkmDataManagerImpl>();
+    ukm_recorder_ = std::make_unique<ukm::TestUkmRecorder>();
+    auto ukm_db = std::make_unique<MockUkmDatabase>();
+    ukm_database_ = ukm_db.get();
+    data_manager_->InitializeForTesting(std::move(ukm_db));
+  }
+
+  void TearDown() override {
+    data_manager_->StopObservingUkm();
+    ukm_recorder_.reset();
+    ukm_database_ = nullptr;
+    data_manager_.reset();
+  }
+
+  void RecordUkmAndWaitForDatabase(ukm::mojom::UkmEntryPtr entry) {}
+
+  TestServicesForPlatform& CreatePlatform() {
+    platform_services_.push_back(
+        std::make_unique<TestServicesForPlatform>(data_manager_.get()));
+    return *platform_services_.back();
+  }
+
+  void RemovePlatform(const TestServicesForPlatform* platform) {
+    auto it = platform_services_.begin();
+    while (it != platform_services_.end()) {
+      if (it->get() == platform) {
+        platform_services_.erase(it);
+        return;
+      }
+      it++;
+    }
+  }
+
+ protected:
+  // Use system time to avoid history service expiration tasks to go into an
+  // infinite loop.
+  base::test::TaskEnvironment task_environment_{
+      base::test::TaskEnvironment::TimeSource::SYSTEM_TIME};
+
+  std::unique_ptr<ukm::TestUkmRecorder> ukm_recorder_;
+  raw_ptr<MockUkmDatabase> ukm_database_;
+  std::unique_ptr<UkmDataManagerImpl> data_manager_;
+  std::vector<std::unique_ptr<TestServicesForPlatform>> platform_services_;
+
+  std::vector<ukm::mojom::UkmEntryPtr> db_entries_;
+};
+
+MATCHER_P(HasEventHash, event_hash, "") {
+  return arg->event_hash == event_hash;
+}
+
+TEST_F(UkmDataManagerImplTest, HistoryNotification) {
+  const GURL kUrl1 = GURL("https://www.url1.com/");
+
+  TestServicesForPlatform& platform1 = CreatePlatform();
+
+  // Add a page to history and check that the notification is sent to
+  // UkmDatabase. All notifications should be sent.
+  base::RunLoop wait_for_add1;
+  EXPECT_CALL(*ukm_database_, OnUrlValidated(kUrl1))
+      .WillOnce([&wait_for_add1]() { wait_for_add1.QuitClosure().Run(); });
+  platform1.history_service->AddPage(kUrl1, base::Time::Now(),
+                                     history::VisitSource::SOURCE_BROWSED);
+  wait_for_add1.Run();
+
+  platform1.history_service->DeleteURLs({kUrl1});
+
+  // Check that RemoveUrls() notification is sent to UkmDatabase.
+  base::RunLoop wait_for_remove1;
+  EXPECT_CALL(*ukm_database_, RemoveUrls(std::vector({kUrl1})))
+      .WillOnce(
+          [&wait_for_remove1]() { wait_for_remove1.QuitClosure().Run(); });
+  wait_for_remove1.Run();
+
+  RemovePlatform(&platform1);
+}
+
+TEST_F(UkmDataManagerImplTest, UkmSourceObservation) {
+  const GURL kUrl1 = GURL("https://www.url1.com/");
+
+  data_manager_->NotifyCanObserveUkm(ukm_recorder_.get());
+
+  // Create a platform that observes PageLoad events.
+  TestServicesForPlatform& platform1 = CreatePlatform();
+  platform1.AddModel(PageLoadModelMetadata());
+
+  // Source updates are notified to the database.
+  base::RunLoop wait_for_source;
+  EXPECT_CALL(*ukm_database_,
+              UpdateUrlForUkmSource(kSourceId, kUrl1, /*is_validated=*/false))
+      .WillOnce([&wait_for_source](ukm::SourceId source_id, const GURL& url,
+                                   bool is_validated) {
+        wait_for_source.QuitClosure().Run();
+      });
+  ukm_recorder_->UpdateSourceURL(kSourceId, kUrl1);
+  wait_for_source.Run();
+
+  RemovePlatform(&platform1);
+}
+
+TEST_F(UkmDataManagerImplTest, UkmEntryObservation) {
+  const GURL kUrl1 = GURL("https://www.url1.com/");
+
+  // UKM added before creating platform do not get recorded.
+  ukm_recorder_->AddEntry(GetSamplePageLoadEntry());
+  ukm_recorder_->AddEntry(GetSamplePaintPreviewEntry());
+
+  // Create a platform that observes PageLoad events.
+  TestServicesForPlatform& platform1 = CreatePlatform();
+  platform1.AddModel(PageLoadModelMetadata());
+
+  // Not added since UkmDataManager is not notified for UKM observation.
+  ukm_recorder_->AddEntry(GetSamplePageLoadEntry());
+
+  data_manager_->NotifyCanObserveUkm(ukm_recorder_.get());
+
+  // Not added since it is not PageLoad event.
+  ukm_recorder_->AddEntry(GetSamplePaintPreviewEntry());
+
+  // PageLoad event gets recorded in UkmDatabase.
+  base::RunLoop wait_for_record;
+  EXPECT_CALL(*ukm_database_,
+              StoreUkmEntry(HasEventHash(PageLoad::kEntryNameHash)))
+      .WillOnce([&wait_for_record](ukm::mojom::UkmEntryPtr entry) {
+        wait_for_record.QuitClosure().Run();
+      });
+  ukm_recorder_->AddEntry(GetSamplePageLoadEntry());
+  wait_for_record.Run();
+
+  RemovePlatform(&platform1);
+}
+
+TEST_F(UkmDataManagerImplTest, UkmServiceCreatedBeforePlatform) {
+  const GURL kUrl1 = GURL("https://www.url1.com/");
+
+  // Observation is available before platforms are created.
+  data_manager_->NotifyCanObserveUkm(ukm_recorder_.get());
+
+  TestServicesForPlatform& platform1 = CreatePlatform();
+  platform1.AddModel(PageLoadModelMetadata());
+
+  // Entry should be recorded, This step does not wait for the database record
+  // here since it is waits for the next observation below.
+  EXPECT_CALL(*ukm_database_,
+              StoreUkmEntry(HasEventHash(PageLoad::kEntryNameHash)));
+  ukm_recorder_->AddEntry(GetSamplePageLoadEntry());
+
+  // Source updates should be notified.
+  base::RunLoop wait_for_source;
+  EXPECT_CALL(*ukm_database_,
+              UpdateUrlForUkmSource(kSourceId, kUrl1, /*is_validated=*/false))
+      .WillOnce([&wait_for_source](ukm::SourceId source_id, const GURL& url,
+                                   bool is_validated) {
+        wait_for_source.QuitClosure().Run();
+      });
+  ukm_recorder_->UpdateSourceURL(kSourceId, kUrl1);
+  wait_for_source.Run();
+
+  RemovePlatform(&platform1);
+}
+
+TEST_F(UkmDataManagerImplTest, UrlValidationWithHistory) {
+  const GURL kUrl1 = GURL("https://www.url1.com/");
+
+  data_manager_->NotifyCanObserveUkm(ukm_recorder_.get());
+  TestServicesForPlatform& platform1 = CreatePlatform();
+  platform1.AddModel(PageLoadModelMetadata());
+
+  // History page is added before source update.
+  base::RunLoop wait_for_add1;
+  EXPECT_CALL(*ukm_database_, OnUrlValidated(kUrl1))
+      .WillOnce([&wait_for_add1]() { wait_for_add1.QuitClosure().Run(); });
+  platform1.history_service->AddPage(kUrl1, base::Time::Now(),
+                                     history::VisitSource::SOURCE_BROWSED);
+  wait_for_add1.Run();
+
+  // Source update should have a validated URL.
+  base::RunLoop wait_for_source;
+  EXPECT_CALL(*ukm_database_,
+              UpdateUrlForUkmSource(kSourceId, kUrl1, /*is_validated=*/true))
+      .WillOnce([&wait_for_source](ukm::SourceId source_id, const GURL& url,
+                                   bool is_validated) {
+        wait_for_source.QuitClosure().Run();
+      });
+  ukm_recorder_->UpdateSourceURL(kSourceId, kUrl1);
+  wait_for_source.Run();
+
+  RemovePlatform(&platform1);
+}
+
+TEST_F(UkmDataManagerImplTest, MultiplePlatforms) {
+  const GURL kUrl1 = GURL("https://www.url1.com/");
+  const GURL kUrl2 = GURL("https://www.url2.com/");
+
+  data_manager_->NotifyCanObserveUkm(ukm_recorder_.get());
+
+  // Create 2 platforms, and 1 of them observing UKM events.
+  TestServicesForPlatform& platform1 = CreatePlatform();
+  TestServicesForPlatform& platform3 = CreatePlatform();
+  platform1.AddModel(PageLoadModelMetadata());
+
+  // Only page load should be added to database.
+  EXPECT_CALL(*ukm_database_,
+              StoreUkmEntry(HasEventHash(PageLoad::kEntryNameHash)));
+  ukm_recorder_->AddEntry(GetSamplePageLoadEntry());
+  ukm_recorder_->AddEntry(GetSamplePaintPreviewEntry());
+
+  // Create another platform observing paint preview.
+  TestServicesForPlatform& platform2 = CreatePlatform();
+  platform2.AddModel(PaintPreviewModelMetadata());
+
+  // Both should be added to database.
+  EXPECT_CALL(*ukm_database_,
+              StoreUkmEntry(HasEventHash(PageLoad::kEntryNameHash)));
+  EXPECT_CALL(*ukm_database_,
+              StoreUkmEntry(HasEventHash(PaintPreviewCapture::kEntryNameHash)));
+  ukm_recorder_->AddEntry(GetSamplePageLoadEntry());
+  ukm_recorder_->AddEntry(GetSamplePaintPreviewEntry());
+
+  // Sources should still be updated.
+  base::RunLoop wait_for_source;
+  EXPECT_CALL(*ukm_database_,
+              UpdateUrlForUkmSource(kSourceId, kUrl1, /*is_validated=*/false))
+      .WillOnce([&wait_for_source](ukm::SourceId source_id, const GURL& url,
+                                   bool is_validated) {
+        wait_for_source.QuitClosure().Run();
+      });
+  ukm_recorder_->UpdateSourceURL(kSourceId, kUrl1);
+  wait_for_source.Run();
+
+  // Removing platform1 does not stop observing metrics.
+  RemovePlatform(&platform1);
+  EXPECT_CALL(*ukm_database_,
+              StoreUkmEntry(HasEventHash(PageLoad::kEntryNameHash)));
+  EXPECT_CALL(*ukm_database_,
+              StoreUkmEntry(HasEventHash(PaintPreviewCapture::kEntryNameHash)));
+  ukm_recorder_->AddEntry(GetSamplePageLoadEntry());
+  ukm_recorder_->AddEntry(GetSamplePaintPreviewEntry());
+
+  // Update history service on one of the platforms, and the database should get
+  // a validated URL.
+  base::RunLoop wait_for_add1;
+  EXPECT_CALL(*ukm_database_, OnUrlValidated(kUrl2))
+      .WillOnce([&wait_for_add1]() { wait_for_add1.QuitClosure().Run(); });
+  platform2.history_service->AddPage(kUrl2, base::Time::Now(),
+                                     history::VisitSource::SOURCE_BROWSED);
+  wait_for_add1.Run();
+
+  base::RunLoop wait_for_source2;
+  EXPECT_CALL(*ukm_database_,
+              UpdateUrlForUkmSource(kSourceId2, kUrl2, /*is_validated=*/true))
+      .WillOnce([&wait_for_source2](ukm::SourceId source_id, const GURL& url,
+                                    bool is_validated) {
+        wait_for_source2.QuitClosure().Run();
+      });
+  ukm_recorder_->UpdateSourceURL(kSourceId2, kUrl2);
+  wait_for_source2.Run();
+
+  RemovePlatform(&platform2);
+  RemovePlatform(&platform3);
+}
+
+}  // namespace segmentation_platform
diff --git a/components/url_formatter/spoof_checks/idn_spoof_checker_unittest.cc b/components/url_formatter/spoof_checks/idn_spoof_checker_unittest.cc
index 4ffccc0..92cd3c6 100644
--- a/components/url_formatter/spoof_checks/idn_spoof_checker_unittest.cc
+++ b/components/url_formatter/spoof_checks/idn_spoof_checker_unittest.cc
@@ -1328,73 +1328,63 @@
 TEST(IDNSpoofCheckerNoFixtureTest, MultipleSkeletons) {
   IDNSpoofChecker checker;
   // apple with U+04CF (ӏ)
-  const GURL url1("http://appӏe.com");
-  const url_formatter::IDNConversionResult result1 =
-      UnsafeIDNToUnicodeWithDetails(url1.host());
-  Skeletons skeletons1 = checker.GetSkeletons(result1.result);
-  EXPECT_EQ(Skeletons({"apple.corn", "appie.corn"}), skeletons1);
-
-  const GURL url2("http://œxamþle.com");
-  const url_formatter::IDNConversionResult result2 =
-      UnsafeIDNToUnicodeWithDetails(url2.host());
-  Skeletons skeletons2 = checker.GetSkeletons(result2.result);
-  // This skeleton set doesn't include strings with "œ" because it gets
-  // converted to "oe" by ICU during skeleton extraction.
-  EXPECT_EQ(Skeletons({"oexarnþle.corn", "oexarnple.corn", "oexarnble.corn",
-                       "cexarnþle.corn", "cexarnple.corn", "cexarnble.corn"}),
-            skeletons2);
+  const GURL url("http://appӏe.com");
+  const url_formatter::IDNConversionResult result =
+      UnsafeIDNToUnicodeWithDetails(url.host());
+  Skeletons skeletons = checker.GetSkeletons(result.result);
+  EXPECT_EQ(Skeletons({"apple.corn", "appie.corn"}), skeletons);
 }
 
 TEST(IDNSpoofCheckerNoFixtureTest, AlternativeSkeletons) {
   struct TestCase {
-    // String whose alternative strings will be generated
-    std::u16string input;
-    // Maximum number of alternative strings to generate.
-    size_t max_alternatives;
-    // Expected string set.
-    base::flat_set<std::u16string> expected_strings;
-  } kTestCases[] = {
-      {u"", 0, {}},
-      {u"", 1, {}},
-      {u"", 2, {}},
-      {u"", 100, {}},
+    // Skeleton whose alternative skeletons will be generated
+    std::string skeleton;
+    // Maximum number of skeletons to generate.
+    size_t max_skeletons;
+    // Expected skeleton set.
+    Skeletons expected_skeletons;
+  } kTestCases[] = {{"", 0, {}},
+                    {"", 1, {}},
+                    {"", 2, {}},
+                    {"", 100, {}},
 
-      {u"a", 0, {}},
-      {u"a", 1, {u"a"}},
-      {u"a", 2, {u"a"}},
-      {u"a", 100, {u"a"}},
+                    {"a", 0, {}},
+                    {"a", 1, {"a"}},
+                    {"a", 2, {"a"}},
+                    {"a", 100, {"a"}},
 
-      {u"ab", 0, {}},
-      {u"ab", 1, {u"ab"}},
-      {u"ab", 2, {u"ab"}},
-      {u"ab", 100, {u"ab"}},
+                    {"ab", 0, {}},
+                    {"ab", 1, {"ab"}},
+                    {"ab", 2, {"ab"}},
+                    {"ab", 100, {"ab"}},
 
-      {u"œ", 0, {}},
-      {u"œ", 1, {u"œ"}},
-      {u"œ", 2, {u"œ", u"ce"}},
-      {u"œ", 100, {u"œ", u"ce", u"oe"}},
+                    {"œ", 0, {}},
+                    {"œ", 1, {"œ"}},
+                    {"œ", 2, {"œ", "ce"}},
+                    {"œ", 100, {"œ", "ce", "oe"}},
 
-      {u"œxample", 0, {}},
-      {u"œxample", 1, {u"œxample"}},
-      {u"œxample", 2, {u"œxample", u"cexample"}},
-      {u"œxample", 100, {u"œxample", u"cexample", u"oexample"}},
+                    {"œxample", 0, {}},
+                    {"œxample", 1, {"œxample"}},
+                    {"œxample", 2, {"œxample", "cexample"}},
+                    {"œxample", 100, {"œxample", "cexample", "oexample"}},
 
-      {u"œxamþle", 0, {}},
-      {u"œxamþle", 1, {u"œxamþle"}},
-      {u"œxamþle", 2, {u"œxamþle", u"œxamble"}},
-      {u"œxamþle",
-       100,
-       {u"œxamþle", u"œxample", u"œxamble", u"oexamþle", u"oexample",
-        u"oexamble", u"cexamþle", u"cexample", u"cexamble"}}};
+                    {"œxamþle", 0, {}},
+                    {"œxamþle", 1, {"œxamþle"}},
+                    {"œxamþle", 2, {"œxamþle", "œxamble"}},
+                    {"œxamþle",
+                     100,
+                     {"œxamþle", "œxample", "œxamble", "oexamþle", "oexample",
+                      "oexamble", "cexamþle", "cexample", "cexamble"}}};
+  // IDNSpoofChecker checker;
   SkeletonMap skeleton_map;
   skeleton_map[u'œ'] = {"ce", "oe"};
   skeleton_map[u'þ'] = {"b", "p"};
 
   for (const TestCase& test_case : kTestCases) {
-    const auto strings = SkeletonGenerator::GenerateSupplementalHostnames(
-        test_case.input, test_case.max_alternatives, skeleton_map);
-    EXPECT_LE(strings.size(), test_case.max_alternatives);
-    EXPECT_EQ(strings, test_case.expected_strings);
+    Skeletons skeletons = SkeletonGenerator::GenerateSupplementalSkeletons(
+        test_case.skeleton, test_case.max_skeletons, skeleton_map);
+    EXPECT_LE(skeletons.size(), test_case.max_skeletons);
+    EXPECT_EQ(skeletons, test_case.expected_skeletons);
   }
 }
 
diff --git a/components/url_formatter/spoof_checks/skeleton_generator.cc b/components/url_formatter/spoof_checks/skeleton_generator.cc
index da557a5..df026d6 100644
--- a/components/url_formatter/spoof_checks/skeleton_generator.cc
+++ b/components/url_formatter/spoof_checks/skeleton_generator.cc
@@ -7,7 +7,6 @@
 #include <ostream>
 
 #include <queue>
-
 #include "base/i18n/unicodestring.h"
 #include "base/memory/ptr_util.h"
 #include "base/strings/string_piece.h"
@@ -21,12 +20,7 @@
 
 using QueueItem = std::vector<std::u16string>;
 
-// Maximum number of supplemental hostname to generate for a given input.
-// If this number is too high, we may end up DOSing the browser process.
-// If it's too low, we may not be able to cover some lookalike URLs.
-const size_t kMaxSupplementalHostnames = 128;
-
-}  // namespace
+}
 
 SkeletonGenerator::SkeletonGenerator(const USpoofChecker* checker)
     : checker_(checker) {
@@ -67,6 +61,7 @@
   //   - {U+0138 (ĸ), U+03BA (κ), U+043A (к), U+049B (қ), U+049D (ҝ),
   //      U+049F (ҟ), U+04A1(ҡ), U+04C4 (ӄ), U+051F (ԟ)} => k
   //   - {U+014B (ŋ), U+043F (п), U+0525 (ԥ), U+0E01 (ก), U+05D7 (ח)} => n
+  //   - U+0153 (œ) => "ce"
   // TODO(crbug/843352): Handle multiple skeletons for U+0525 and U+0153.
   //   - {U+0167 (ŧ), U+0442 (т), U+04AD (ҭ), U+050F (ԏ), U+4E03 (七),
   //     U+4E05 (丅), U+4E06 (丆), U+4E01 (丁)} => t
@@ -106,7 +101,7 @@
           UNICODE_STRING_SIMPLE("ExtraConf"),
           icu::UnicodeString::fromUTF8(
               "[æӕ] > ae; [ϼҏ] > p; [ħнћңҥӈӊԋԧԩ] > h;"
-              "[ĸκкқҝҟҡӄԟ] > k; [ŋпԥกח] > n;"
+              "[ĸκкқҝҟҡӄԟ] > k; [ŋпԥกח] > n; œ > ce;"
               "[ŧтҭԏ七丅丆丁] > t; [ƅьҍв] > b;  [ωшщพฟພຟ] > w;"
               "[мӎ] > m; [єҽҿၔ] > e; ґ > r; [ғӻ] > f;"
               "[ҫင] > c; [ұ丫] > y; [χҳӽӿ乂] > x;"
@@ -126,10 +121,6 @@
   DCHECK(U_SUCCESS(status))
       << "Skeleton generator initialization failed due to an error: "
       << u_errorName(status);
-
-  // Characters that look like multiple characters.
-  character_map_[u'þ'] = {"b", "p"};
-  character_map_[u'œ'] = {"ce", "oe"};
 }
 
 SkeletonGenerator::~SkeletonGenerator() = default;
@@ -150,34 +141,25 @@
   return base::i18n::UnicodeStringToString16(host);
 }
 
-Skeletons SkeletonGenerator::GetSkeletons(base::StringPiece16 input_hostname) {
-  std::u16string hostname_no_diacritics = MaybeRemoveDiacritics(input_hostname);
-
-  // Generate alternative versions of the input hostname and extract skeletons.
+Skeletons SkeletonGenerator::GetSkeletons(base::StringPiece16 hostname) {
   Skeletons skeletons;
-  for (const std::u16string& hostname : GenerateSupplementalHostnames(
-           hostname_no_diacritics, kMaxSupplementalHostnames, character_map_)) {
-    size_t hostname_length =
-        hostname.length() - (hostname.back() == '.' ? 1 : 0);
-    icu::UnicodeString hostname_unicode(false, hostname.data(),
-                                        hostname_length);
-    extra_confusable_mapper_->transliterate(hostname_unicode);
+  size_t hostname_length = hostname.length() - (hostname.back() == '.' ? 1 : 0);
+  icu::UnicodeString host(false, hostname.data(), hostname_length);
+  MaybeRemoveDiacritics(host);
+  extra_confusable_mapper_->transliterate(host);
 
-    UErrorCode status = U_ZERO_ERROR;
-    icu::UnicodeString ustr_skeleton;
+  UErrorCode status = U_ZERO_ERROR;
+  icu::UnicodeString ustr_skeleton;
 
-    // Map U+04CF (ӏ) to lowercase L in addition to what uspoof_getSkeleton does
-    // (mapping it to lowercase I).
-    AddSkeletonMapping(hostname_unicode, 0x4CF /* ӏ */, 0x6C /* lowercase L */,
-                       &skeletons);
+  // Map U+04CF (ӏ) to lowercase L in addition to what uspoof_getSkeleton does
+  // (mapping it to lowercase I).
+  AddSkeletonMapping(host, 0x4CF /* ӏ */, 0x6C /* lowercase L */, &skeletons);
 
-    uspoof_getSkeletonUnicodeString(checker_, 0, hostname_unicode,
-                                    ustr_skeleton, &status);
-    if (U_SUCCESS(status)) {
-      std::string skeleton;
-      ustr_skeleton.toUTF8String(skeleton);
-      skeletons.insert(skeleton);
-    }
+  uspoof_getSkeletonUnicodeString(checker_, 0, host, ustr_skeleton, &status);
+  if (U_SUCCESS(status)) {
+    std::string skeleton;
+    ustr_skeleton.toUTF8String(skeleton);
+    skeletons.insert(skeleton);
   }
   return skeletons;
 }
@@ -215,19 +197,18 @@
 }
 
 // static
-base::flat_set<std::u16string> SkeletonGenerator::GenerateSupplementalHostnames(
-    base::StringPiece16 input,
+Skeletons SkeletonGenerator::GenerateSupplementalSkeletons(
+    base::StringPiece input,
     size_t max_alternatives,
     const SkeletonMap& mapping) {
-  base::flat_set<std::u16string> output;
   if (!input.size() || max_alternatives == 0) {
-    return output;
+    return Skeletons();
   }
-  icu::UnicodeString input_unicode =
-      icu::UnicodeString::fromUTF8(base::UTF16ToUTF8(input));
+  icu::UnicodeString input_unicode = icu::UnicodeString::fromUTF8(input);
   // Read only buffer, doesn't need to be released.
   const char16_t* input_buffer = input_unicode.getBuffer();
 
+  Skeletons output;
   // This queue contains vectors of skeleton strings. For each character in
   // the input string, its skeleton string will be appended to the queue item.
   // Thus, the number of skeleton strings in the queue item will always
@@ -242,7 +223,7 @@
     if (current.size() == static_cast<size_t>(input_unicode.length())) {
       // Reached the end of the original string. We now generated a complete
       // alternative string. Add the result to output.
-      output.insert(base::JoinString(current, u""));
+      output.insert(base::UTF16ToUTF8(base::JoinString(current, u"")));
       if (output.size() == max_alternatives) {
         break;
       }
diff --git a/components/url_formatter/spoof_checks/skeleton_generator.h b/components/url_formatter/spoof_checks/skeleton_generator.h
index 114371a..401c6ff3 100644
--- a/components/url_formatter/spoof_checks/skeleton_generator.h
+++ b/components/url_formatter/spoof_checks/skeleton_generator.h
@@ -37,26 +37,18 @@
 // 1. The hostname is "normalized" by removing its diacritics. This is done so
 //    that more confusable hostnames can be detected than would be using the
 //    plain ICU API.
-// 2. Supplemental hostname strings are generated from the normalized hostname
-//    using a manually curated "multiple skeleton" table. This table has a
-//    one-to-many relationship between characters and their skeletons. The
-//    number of skeletons generated by this step is capped to a maximum number.
-//    This step is done before ICU's skeleton generation (which is many-to-one)
-//    so that we can generate more supplemental hostnames. For example, ICU
-//    maps "œ" to "oe". Since the character "œ" won't appear in the ICU
-//    skeleton, we can't produce supplemental skeletons for it. Therefore, we
-//    must map it to "oe" and "ce" before skeleton generation.
-// 3. For each supplemental hostname, the following steps are performed:
-// 4. Certain characters in the hostname are mapped to their confusable
-//    equivalents using a manually curated table (extra confusible mapper). This
-//    table has a many-to-one relationship between characters and their
-//    skeletons. For example, the characters є, ҽ, ҿ, and ၔ are all
+// 2. Certain characters in the normalized hostname are mapped to their
+//    confusable equivalents using a manually curated table (extra confusable
+//    mapper). This table has a many-to-one relationship between characters and
+//    their skeletons. For example, the characters є, ҽ, ҿ, and ၔ are all
 //    mapped to Latin lowercase e.
-// 5. The hostname is passed to ICU to generate actual skeleton strings.
-// 6. If the character U+04CF (ӏ) is present in the skeleton, another skeleton
+// 3. The hostname is passed to ICU to generate actual skeleton strings.
+// 3. If the character U+04CF (ӏ) is present in the skeleton, another skeleton
 //    is generated by mapping it to lowercase L (U+6C).
-// 7. The final output is a Skeletons instance which contains one or more
-//    skeleton strings that represent the input hostname.
+// 4. Finally, alternative skeletons are generated from the skeleton set using
+//    a manually curated "multiple skeleton" table. This table has a one-to-many
+//    relationship between characters and their skeletons. The number of
+//    skeletons generated by this step is capped to a maximum number.
 class SkeletonGenerator {
  public:
   explicit SkeletonGenerator(const USpoofChecker* checker);
@@ -76,12 +68,11 @@
   std::u16string MaybeRemoveDiacritics(base::StringPiece16 hostname);
 
   // Returns the set of alternative strings using the one-to-many string
-  // mapping provided in `mapping`. Generates at most `max_alternatives` strings
-  // from the input string.
-  static base::flat_set<std::u16string> GenerateSupplementalHostnames(
-      base::StringPiece16 input,
-      size_t max_alternatives,
-      const SkeletonMap& mapping);
+  // mapping. Generates at most `max_alternatives` strings from the input
+  // string.
+  static Skeletons GenerateSupplementalSkeletons(base::StringPiece skeleton,
+                                                 size_t max_alternatives,
+                                                 const SkeletonMap& mapping);
 
  private:
   // Adds an additional mapping from |src_char| to |mapped_char| when generating
@@ -99,9 +90,6 @@
   std::unique_ptr<icu::Transliterator> diacritic_remover_;
   std::unique_ptr<icu::Transliterator> extra_confusable_mapper_;
 
-  // Map of characters to their skeletons. This map is manually curated.
-  std::map<char16_t, Skeletons> character_map_;
-
   raw_ptr<const USpoofChecker> checker_;
 };
 
diff --git a/components/viz/common/features.cc b/components/viz/common/features.cc
index 7f83f77d..f66e1d5f 100644
--- a/components/viz/common/features.cc
+++ b/components/viz/common/features.cc
@@ -42,7 +42,7 @@
 
 const base::Feature kEnableOverlayPrioritization {
   "EnableOverlayPrioritization",
-#if BUILDFLAG(USE_CHROMEOS_PROTECTED_MEDIA)
+#if BUILDFLAG(IS_CHROMEOS_ASH) || BUILDFLAG(IS_CHROMEOS_LACROS)
       base::FEATURE_ENABLED_BY_DEFAULT
 #else
       base::FEATURE_DISABLED_BY_DEFAULT
diff --git a/components/viz/service/frame_sinks/compositor_frame_sink_support.cc b/components/viz/service/frame_sinks/compositor_frame_sink_support.cc
index 79a44687..51c829c9 100644
--- a/components/viz/service/frame_sinks/compositor_frame_sink_support.cc
+++ b/components/viz/service/frame_sinks/compositor_frame_sink_support.cc
@@ -894,35 +894,56 @@
 
   // We require a begin frame if there's a callback pending, or if the client
   // requested it, or if the client needs to get some frame timing details.
-  bool needs_begin_frame =
+  needs_begin_frame_ =
       (client_needs_begin_frame_ || !frame_timing_details_.empty() ||
        !pending_surfaces_.empty() ||
        (compositor_frame_callback_ && !callback_received_begin_frame_) ||
-       surface_animation_manager_.NeedsBeginFrame()) &&
-      !bundle_id_.has_value();
+       surface_animation_manager_.NeedsBeginFrame());
 
-  if (needs_begin_frame == added_frame_observer_)
+  if (bundle_id_.has_value()) {
+    // When bundled with other sinks, observation of BeginFrame notifications is
+    // always delegated to the bundle.
+    if (added_frame_observer_) {
+      StopObservingBeginFrameSource();
+    }
+    if (auto* bundle = frame_sink_manager_->GetFrameSinkBundle(*bundle_id_)) {
+      bundle->SetSinkNeedsBeginFrame(frame_sink_id_.sink_id(),
+                                     needs_begin_frame_);
+    }
+    return;
+  }
+
+  if (needs_begin_frame_ == added_frame_observer_)
     return;
 
-  added_frame_observer_ = needs_begin_frame;
-  if (needs_begin_frame) {
-    begin_frame_source_->AddObserver(this);
-    if (power_mode_voter_) {
-      power_mode_voter_->VoteFor(
-          frame_sink_type_ == mojom::CompositorFrameSinkType::kMediaStream ||
-                  frame_sink_type_ == mojom::CompositorFrameSinkType::kVideo
-              ? power_scheduler::PowerMode::kVideoPlayback
-              : power_scheduler::PowerMode::kAnimation);
-    }
+  if (needs_begin_frame_) {
+    StartObservingBeginFrameSource();
   } else {
-    begin_frame_source_->RemoveObserver(this);
-    if (power_mode_voter_) {
-      power_mode_voter_->ResetVoteAfterTimeout(
-          frame_sink_type_ == mojom::CompositorFrameSinkType::kMediaStream ||
-                  frame_sink_type_ == mojom::CompositorFrameSinkType::kVideo
-              ? power_scheduler::PowerModeVoter::kVideoTimeout
-              : power_scheduler::PowerModeVoter::kAnimationTimeout);
-    }
+    StopObservingBeginFrameSource();
+  }
+}
+
+void CompositorFrameSinkSupport::StartObservingBeginFrameSource() {
+  added_frame_observer_ = true;
+  begin_frame_source_->AddObserver(this);
+  if (power_mode_voter_) {
+    power_mode_voter_->VoteFor(
+        frame_sink_type_ == mojom::CompositorFrameSinkType::kMediaStream ||
+                frame_sink_type_ == mojom::CompositorFrameSinkType::kVideo
+            ? power_scheduler::PowerMode::kVideoPlayback
+            : power_scheduler::PowerMode::kAnimation);
+  }
+}
+
+void CompositorFrameSinkSupport::StopObservingBeginFrameSource() {
+  added_frame_observer_ = false;
+  begin_frame_source_->RemoveObserver(this);
+  if (power_mode_voter_) {
+    power_mode_voter_->ResetVoteAfterTimeout(
+        frame_sink_type_ == mojom::CompositorFrameSinkType::kMediaStream ||
+                frame_sink_type_ == mojom::CompositorFrameSinkType::kVideo
+            ? power_scheduler::PowerModeVoter::kVideoTimeout
+            : power_scheduler::PowerModeVoter::kAnimationTimeout);
   }
 }
 
diff --git a/components/viz/service/frame_sinks/compositor_frame_sink_support.h b/components/viz/service/frame_sinks/compositor_frame_sink_support.h
index 9dc79a4b3..f3a5548 100644
--- a/components/viz/service/frame_sinks/compositor_frame_sink_support.h
+++ b/components/viz/service/frame_sinks/compositor_frame_sink_support.h
@@ -107,6 +107,8 @@
     return frame_timing_details_;
   }
 
+  bool needs_begin_frame() const { return needs_begin_frame_; }
+
   [[nodiscard]] FrameTimingDetailsMap TakeFrameTimingDetailsMap();
 
   // Viz hit-test setup is only called when |is_root_| is true (except on
@@ -271,6 +273,8 @@
   bool IsRoot() const override;
 
   void UpdateNeedsBeginFramesInternal();
+  void StartObservingBeginFrameSource();
+  void StopObservingBeginFrameSource();
 
   // For the sync API calls, if we are blocking a client callback, runs it once
   // BeginFrame and FrameAck are done.
@@ -332,6 +336,9 @@
   // Whether a request for begin frames has been issued.
   bool client_needs_begin_frame_ = false;
 
+  // Whether the sink currently needs begin frames for any reason.
+  bool needs_begin_frame_ = false;
+
   // Whether or not a frame observer has been added.
   bool added_frame_observer_ = false;
 
diff --git a/components/viz/service/frame_sinks/frame_sink_bundle_impl.cc b/components/viz/service/frame_sinks/frame_sink_bundle_impl.cc
index 4bf3b45..22346e8 100644
--- a/components/viz/service/frame_sinks/frame_sink_bundle_impl.cc
+++ b/components/viz/service/frame_sinks/frame_sink_bundle_impl.cc
@@ -23,26 +23,56 @@
 // bundled CompositorFrameSink clients who all share a common BeginFrameSource.
 // FrameSinkBundleImpls may own any number of SinkGroups, and groups are created
 // or destroyed as needed when a sink is added to or removed from the bundle.
+//
+// Note that the BeginFrameSource is only observed by this SinkGroup while there
+// are active FrameSinks present who have explicitly indicated a need for
+// BeginFrame notifications. This avoids generation and processing of unused
+// frame events which might otherwise incur substantial overhead.
 class FrameSinkBundleImpl::SinkGroup : public BeginFrameObserver {
  public:
   SinkGroup(FrameSinkManagerImpl& manager,
             FrameSinkBundleImpl& bundle,
             BeginFrameSource& source,
             mojom::FrameSinkBundleClient& client)
-      : manager_(manager), bundle_(bundle), source_(source), client_(client) {
-    source_.AddObserver(this);
-  }
+      : manager_(manager), bundle_(bundle), source_(source), client_(client) {}
 
-  ~SinkGroup() override { source_.RemoveObserver(this); }
+  ~SinkGroup() override {
+    if (is_observing_begin_frame_) {
+      source_.RemoveObserver(this);
+    }
+  }
 
   bool IsEmpty() const { return frame_sinks_.empty(); }
 
-  void AddFrameSink(uint32_t sink_id) { frame_sinks_.insert(sink_id); }
+  void AddFrameSink(uint32_t sink_id) {
+    frame_sinks_.insert(sink_id);
+
+    FrameSinkId id(bundle_.id().client_id(), sink_id);
+    if (auto* support = manager_.GetFrameSinkForId(id)) {
+      if (support->needs_begin_frame()) {
+        frame_sinks_needing_begin_frame_.insert(sink_id);
+        UpdateBeginFrameObservation();
+      }
+    }
+  }
 
   void RemoveFrameSink(uint32_t sink_id) {
     frame_sinks_.erase(sink_id);
     unacked_submissions_.erase(sink_id);
     FlushMessages();
+
+    frame_sinks_needing_begin_frame_.erase(sink_id);
+    UpdateBeginFrameObservation();
+  }
+
+  void SetNeedsBeginFrame(uint32_t sink_id, bool needs_begin_frame) {
+    if (needs_begin_frame) {
+      frame_sinks_needing_begin_frame_.insert(sink_id);
+    } else {
+      frame_sinks_needing_begin_frame_.erase(sink_id);
+    }
+
+    UpdateBeginFrameObservation();
   }
 
   void WillSubmitFrame(uint32_t sink_id) {
@@ -136,6 +166,23 @@
   }
 
  private:
+  void UpdateBeginFrameObservation() {
+    bool should_observe_begin_frame = !frame_sinks_needing_begin_frame_.empty();
+    if (should_observe_begin_frame && !is_observing_begin_frame_) {
+      // NOTE: It's important to set this flag before adding the observer,
+      // because AddObserver() can synchronously enter CFSS::OnBeginFrame(),
+      // which can in turn re-enter this method.
+      is_observing_begin_frame_ = true;
+      source_.AddObserver(this);
+      return;
+    }
+
+    if (is_observing_begin_frame_ && !should_observe_begin_frame) {
+      source_.RemoveObserver(this);
+      is_observing_begin_frame_ = false;
+    }
+  }
+
   FrameSinkManagerImpl& manager_;
   FrameSinkBundleImpl& bundle_;
   BeginFrameSource& source_;
@@ -146,6 +193,8 @@
   std::vector<mojom::BundledReturnedResourcesPtr> pending_reclaimed_resources_;
   std::vector<mojom::BeginFrameInfoPtr> pending_on_begin_frames_;
   std::set<uint32_t> frame_sinks_;
+  std::set<uint32_t> frame_sinks_needing_begin_frame_;
+  bool is_observing_begin_frame_ = false;
 
   // Tracks which sinks in the group are still expecting an ack for a previously
   // submitted frame.
@@ -169,6 +218,13 @@
 
 FrameSinkBundleImpl::~FrameSinkBundleImpl() = default;
 
+void FrameSinkBundleImpl::SetSinkNeedsBeginFrame(uint32_t sink_id,
+                                                 bool needs_begin_frame) {
+  if (auto* group = GetSinkGroup(sink_id)) {
+    group->SetNeedsBeginFrame(sink_id, needs_begin_frame);
+  }
+}
+
 void FrameSinkBundleImpl::AddFrameSink(CompositorFrameSinkSupport* support) {
   uint32_t sink_id = support->frame_sink_id().sink_id();
   auto* source = support->begin_frame_source();
diff --git a/components/viz/service/frame_sinks/frame_sink_bundle_impl.h b/components/viz/service/frame_sinks/frame_sink_bundle_impl.h
index 85d1de3a..f7b958ad 100644
--- a/components/viz/service/frame_sinks/frame_sink_bundle_impl.h
+++ b/components/viz/service/frame_sinks/frame_sink_bundle_impl.h
@@ -51,6 +51,11 @@
 
   const FrameSinkBundleId& id() const { return id_; }
 
+  // Called by the identified sink itself to notify the bundle that the sink
+  // needs (or no longer needs) BeginFrame notifications. This is distinct from
+  // SetNeedsBeginFrame(), as the latter is only called by clients.
+  void SetSinkNeedsBeginFrame(uint32_t sink_id, bool needs_begin_frame);
+
   void AddFrameSink(CompositorFrameSinkSupport* support);
   void UpdateFrameSink(CompositorFrameSinkSupport* support,
                        BeginFrameSource* old_source);
diff --git a/components/viz/service/frame_sinks/frame_sink_bundle_impl_unittest.cc b/components/viz/service/frame_sinks/frame_sink_bundle_impl_unittest.cc
index 87e67a7..0b8095e 100644
--- a/components/viz/service/frame_sinks/frame_sink_bundle_impl_unittest.cc
+++ b/components/viz/service/frame_sinks/frame_sink_bundle_impl_unittest.cc
@@ -266,6 +266,9 @@
   FrameSinkManagerImpl& manager() { return manager_; }
   TestBundleClient& test_client() { return test_client_; }
   mojo::Remote<mojom::FrameSinkBundle>& bundle() { return bundle_; }
+  FakeExternalBeginFrameSource& begin_frame_source() {
+    return begin_frame_source_;
+  }
 
  private:
   const gpu::SyncToken frame_sync_token_{MakeVerifiedSyncToken(42)};
@@ -300,10 +303,18 @@
 }
 
 TEST_F(FrameSinkBundleImplTest, OnBeginFrame) {
+  // By default the bundle does not observe the BeginFrameSource. The only
+  // observer is the (non-bundled) main-frame sink.
+  EXPECT_EQ(1u, begin_frame_source().num_observers());
+
   TestFrameSink frame_a(manager(), kSubFrameA, kMainFrame, kBundleId);
   TestFrameSink frame_b(manager(), kSubFrameB, kMainFrame, kBundleId);
   TestFrameSink frame_c(manager(), kSubFrameC, kMainFrame, kBundleId);
 
+  // The bundle should observe the BeginFrameSource on behalf of all its sinks,
+  // so the only observers should now be the main-frame sink and the bundle.
+  EXPECT_EQ(2u, begin_frame_source().num_observers());
+
   // OnBeginFrame() should elicit a single batch of notifications to the bundle
   // client, with a notification for each frame in the bundle.
   std::vector<mojom::BeginFrameInfoPtr> begin_frames;
@@ -320,6 +331,14 @@
   test_client().WaitForNextFlush(nullptr, &begin_frames, nullptr);
   EXPECT_THAT(begin_frames,
               UnorderedElementsAre(ForSink(kSubFrameA), ForSink(kSubFrameC)));
+
+  // Finally, if all sinks unsubscribe from BeginFrame notifications, the bundle
+  // should stop observing the BeginFrameSource.
+  EXPECT_EQ(2u, begin_frame_source().num_observers());
+  manager().GetFrameSinkForId(kSubFrameA)->SetNeedsBeginFrame(false);
+  EXPECT_EQ(2u, begin_frame_source().num_observers());
+  manager().GetFrameSinkForId(kSubFrameC)->SetNeedsBeginFrame(false);
+  EXPECT_EQ(1u, begin_frame_source().num_observers());
 }
 
 TEST_F(FrameSinkBundleImplTest, SubmitAndAck) {
diff --git a/components/zucchini/BUILD.gn b/components/zucchini/BUILD.gn
index 9fa2ea9..6f55fd0 100644
--- a/components/zucchini/BUILD.gn
+++ b/components/zucchini/BUILD.gn
@@ -76,8 +76,6 @@
     "patch_utils.h",
     "patch_writer.cc",
     "patch_writer.h",
-    "reference_bytes_mixer.cc",
-    "reference_bytes_mixer.h",
     "reference_set.cc",
     "reference_set.h",
     "rel32_finder.cc",
diff --git a/components/zucchini/disassembler.cc b/components/zucchini/disassembler.cc
index 4a210acd..beb5f9ab 100644
--- a/components/zucchini/disassembler.cc
+++ b/components/zucchini/disassembler.cc
@@ -42,6 +42,15 @@
   return (disasm->*writer_factory_)(image);
 }
 
+std::unique_ptr<ReferenceMixer> ReferenceGroup::GetMixer(
+    ConstBufferView old_image,
+    ConstBufferView new_image,
+    Disassembler* disasm) const {
+  if (mixer_factory_)
+    return (disasm->*mixer_factory_)(old_image, new_image);
+  return nullptr;
+}
+
 /******** Disassembler ********/
 
 Disassembler::Disassembler(int num_equivalence_iterations)
diff --git a/components/zucchini/disassembler.h b/components/zucchini/disassembler.h
index 48ee0fb..c676e60 100644
--- a/components/zucchini/disassembler.h
+++ b/components/zucchini/disassembler.h
@@ -102,6 +102,10 @@
   using WriterFactory = std::unique_ptr<ReferenceWriter> (Disassembler::*)(
       MutableBufferView image);
 
+  // Member function pointer used to obtain a ReferenceMixer.
+  using MixerFactory = std::unique_ptr<ReferenceMixer> (
+      Disassembler::*)(ConstBufferView old_image, ConstBufferView new_image);
+
   // RefinedGeneratorFactory and RefinedReceptorFactory don't have to be
   // identical to GeneratorFactory and ReceptorFactory, but they must be
   // convertible. As a result, they can be pointer to member function of a
@@ -114,6 +118,18 @@
         reader_factory_(static_cast<ReaderFactory>(reader_factory)),
         writer_factory_(static_cast<WriterFactory>(writer_factory)) {}
 
+  template <class RefinedReaderFactory,
+            class RefinedWriterFactory,
+            class RefinedMixerFactory>
+  ReferenceGroup(ReferenceTypeTraits traits,
+                 RefinedReaderFactory reader_factory,
+                 RefinedWriterFactory writer_factory,
+                 RefinedMixerFactory mixer_factory)
+      : traits_(traits),
+        reader_factory_(static_cast<ReaderFactory>(reader_factory)),
+        writer_factory_(static_cast<WriterFactory>(writer_factory)),
+        mixer_factory_(static_cast<MixerFactory>(mixer_factory)) {}
+
   // Returns a reader for all references in the binary.
   // Invalidates any other writer or reader previously obtained for |disasm|.
   std::unique_ptr<ReferenceReader> GetReader(Disassembler* disasm) const;
@@ -131,6 +147,12 @@
   std::unique_ptr<ReferenceWriter> GetWriter(MutableBufferView image,
                                              Disassembler* disasm) const;
 
+  // Returns mixer for references between |old_image| and |new_image|, assuming
+  // they both contain the same type of executable as |disasm|.
+  std::unique_ptr<ReferenceMixer> GetMixer(ConstBufferView old_image,
+                                           ConstBufferView new_image,
+                                           Disassembler* disasm) const;
+
   // Returns traits describing the reference type.
   const ReferenceTypeTraits& traits() const { return traits_; }
 
@@ -147,6 +169,7 @@
   ReferenceTypeTraits traits_;
   ReaderFactory reader_factory_ = nullptr;
   WriterFactory writer_factory_ = nullptr;
+  MixerFactory mixer_factory_ = nullptr;
 };
 
 }  // namespace zucchini
diff --git a/components/zucchini/disassembler_elf.cc b/components/zucchini/disassembler_elf.cc
index 22a29bab..524d118f 100644
--- a/components/zucchini/disassembler_elf.cc
+++ b/components/zucchini/disassembler_elf.cc
@@ -614,6 +614,14 @@
                                                        image);
 }
 
+template <class TRAITS>
+template <class ADDR_TRAITS>
+std::unique_ptr<ReferenceMixer> DisassemblerElfArm<TRAITS>::MakeMixRel32(
+    ConstBufferView src_image,
+    ConstBufferView dst_image) {
+  return std::make_unique<Rel32MixerArm<ADDR_TRAITS>>(src_image, dst_image);
+}
+
 /******** DisassemblerElfAArch32 ********/
 
 DisassemblerElfAArch32::DisassemblerElfAArch32() = default;
@@ -637,30 +645,40 @@
        &DisassemblerElfAArch32::MakeReadRel32<
            AArch32Rel32Translator::AddrTraits_A24>,
        &DisassemblerElfAArch32::MakeWriteRel32<
+           AArch32Rel32Translator::AddrTraits_A24>,
+       &DisassemblerElfAArch32::MakeMixRel32<
            AArch32Rel32Translator::AddrTraits_A24>},
       {ReferenceTypeTraits{2, TypeTag(AArch32ReferenceType::kRel32_T8),
                            PoolTag(ArmReferencePool::kPoolRel32)},
        &DisassemblerElfAArch32::MakeReadRel32<
            AArch32Rel32Translator::AddrTraits_T8>,
        &DisassemblerElfAArch32::MakeWriteRel32<
+           AArch32Rel32Translator::AddrTraits_T8>,
+       &DisassemblerElfAArch32::MakeMixRel32<
            AArch32Rel32Translator::AddrTraits_T8>},
       {ReferenceTypeTraits{2, TypeTag(AArch32ReferenceType::kRel32_T11),
                            PoolTag(ArmReferencePool::kPoolRel32)},
        &DisassemblerElfAArch32::MakeReadRel32<
            AArch32Rel32Translator::AddrTraits_T11>,
        &DisassemblerElfAArch32::MakeWriteRel32<
+           AArch32Rel32Translator::AddrTraits_T11>,
+       &DisassemblerElfAArch32::MakeMixRel32<
            AArch32Rel32Translator::AddrTraits_T11>},
       {ReferenceTypeTraits{4, TypeTag(AArch32ReferenceType::kRel32_T20),
                            PoolTag(ArmReferencePool::kPoolRel32)},
        &DisassemblerElfAArch32::MakeReadRel32<
            AArch32Rel32Translator::AddrTraits_T20>,
        &DisassemblerElfAArch32::MakeWriteRel32<
+           AArch32Rel32Translator::AddrTraits_T20>,
+       &DisassemblerElfAArch32::MakeMixRel32<
            AArch32Rel32Translator::AddrTraits_T20>},
       {ReferenceTypeTraits{4, TypeTag(AArch32ReferenceType::kRel32_T24),
                            PoolTag(ArmReferencePool::kPoolRel32)},
        &DisassemblerElfAArch32::MakeReadRel32<
            AArch32Rel32Translator::AddrTraits_T24>,
        &DisassemblerElfAArch32::MakeWriteRel32<
+           AArch32Rel32Translator::AddrTraits_T24>,
+       &DisassemblerElfAArch32::MakeMixRel32<
            AArch32Rel32Translator::AddrTraits_T24>},
   };
 }
@@ -725,18 +743,24 @@
        &DisassemblerElfAArch64::MakeReadRel32<
            AArch64Rel32Translator::AddrTraits_Immd14>,
        &DisassemblerElfAArch64::MakeWriteRel32<
+           AArch64Rel32Translator::AddrTraits_Immd14>,
+       &DisassemblerElfAArch32::MakeMixRel32<
            AArch64Rel32Translator::AddrTraits_Immd14>},
       {ReferenceTypeTraits{4, TypeTag(AArch64ReferenceType::kRel32_Immd19),
                            PoolTag(ArmReferencePool::kPoolRel32)},
        &DisassemblerElfAArch64::MakeReadRel32<
            AArch64Rel32Translator::AddrTraits_Immd19>,
        &DisassemblerElfAArch64::MakeWriteRel32<
+           AArch64Rel32Translator::AddrTraits_Immd19>,
+       &DisassemblerElfAArch32::MakeMixRel32<
            AArch64Rel32Translator::AddrTraits_Immd19>},
       {ReferenceTypeTraits{4, TypeTag(AArch64ReferenceType::kRel32_Immd26),
                            PoolTag(ArmReferencePool::kPoolRel32)},
        &DisassemblerElfAArch64::MakeReadRel32<
            AArch64Rel32Translator::AddrTraits_Immd26>,
        &DisassemblerElfAArch64::MakeWriteRel32<
+           AArch64Rel32Translator::AddrTraits_Immd26>,
+       &DisassemblerElfAArch32::MakeMixRel32<
            AArch64Rel32Translator::AddrTraits_Immd26>},
   };
 }
diff --git a/components/zucchini/disassembler_elf.h b/components/zucchini/disassembler_elf.h
index 8b834fa2..7368e6e 100644
--- a/components/zucchini/disassembler_elf.h
+++ b/components/zucchini/disassembler_elf.h
@@ -300,13 +300,17 @@
   std::unique_ptr<ReferenceReader> MakeReadAbs32(offset_t lo, offset_t hi);
   std::unique_ptr<ReferenceWriter> MakeWriteAbs32(MutableBufferView image);
 
-  // Specialized Read/Write functions for different rel32 address types.
+  // Specialized Read/Write/Mix functions for different rel32 address types.
   template <class ADDR_TRAITS>
   std::unique_ptr<ReferenceReader> MakeReadRel32(offset_t lower,
                                                  offset_t upper);
   template <class ADDR_TRAITS>
   std::unique_ptr<ReferenceWriter> MakeWriteRel32(MutableBufferView image);
 
+  template <class ADDR_TRAITS>
+  std::unique_ptr<ReferenceMixer> MakeMixRel32(ConstBufferView old_image,
+                                               ConstBufferView new_image);
+
  protected:
   // Sorted file offsets of rel32 locations for each rel32 address type.
   std::deque<offset_t>
diff --git a/components/zucchini/image_utils.h b/components/zucchini/image_utils.h
index 748e20b2..5272c5f 100644
--- a/components/zucchini/image_utils.h
+++ b/components/zucchini/image_utils.h
@@ -108,6 +108,56 @@
   virtual void PutNext(Reference reference) = 0;
 };
 
+// References encoding may be quite complex in some architectures (e.g., ARM),
+// requiring bit-level manipulation. In general, bits in a reference body fall
+// under 2 categories:
+// * Operation bits: Instruction op code, conditionals, or structural data.
+// * Payload bits: Actual target data of the reference. These may be absolute,
+//   or be displacements relative to instruction pointer / program counter.
+// During patch application,
+//   Old reference bytes = {old operation, old payload},
+// is transformed to
+//   New reference bytes = {new operation, new payload}.
+// New image bytes are written by three sources:
+//   (1) Direct copy from old image to new image for matched blocks.
+//   (2) Bytewise diff correction.
+//   (3) Dedicated reference target correction.
+//
+// For references whose operation and payload bits are stored in easily
+// separable bytes (e.g., rel32 reference in X86), (2) can exclude payload bits.
+// So during patch application, (1) naively copies everything, (2) fixes
+// operation bytes only, and (3) fixes payload bytes only.
+//
+// For architectures with references whose operation and payload bits may mix
+// within shared bytes (e.g., ARM rel32), a dilemma arises:
+// * (2) cannot ignores shared bytes, since otherwise new operation bits would
+//   not properly transfer.
+// * Having (2) always overwrite these bytes would reduce the benefits of
+//   reference correction, since references are likely to change.
+//
+// Our solution applies a hybrid approach: For each matching old / new reference
+// pair, define:
+//   Mixed reference bytes = {new operation, old payload},
+//
+// During patch generation, we compute bytewise correction from old reference
+// bytes to the mixed reference bytes. So during patch application, (2) only
+// corrects operation bit changes (and skips if they don't change), and (3)
+// overwrites old payload bits to new payload bits.
+
+// Interface for mixed reference byte generation. This base class
+// serves as a stub. Architectures whose references store operation bits and
+// payload bits can share common bytes (e.g., ARM rel32) should override this.
+class ReferenceMixer {
+ public:
+  virtual ~ReferenceMixer() = default;
+
+  // Computes mixed reference bytes by combining (a) "payload bits" from an
+  // "old" reference at |old_offset| with (b) "operation bits" from a "new"
+  // reference at |new_offset|. Returns the result as ConstBufferView, which is
+  // valid only until the next call to Mix().
+  virtual ConstBufferView Mix(offset_t old_offset, offset_t new_offset) = 0;
+};
+
 // An Equivalence is a block of length |length| that approximately match in
 // |old_image| at an offset of |src_offset| and in |new_image| at an offset of
 // |dst_offset|.
diff --git a/components/zucchini/reference_bytes_mixer.cc b/components/zucchini/reference_bytes_mixer.cc
deleted file mode 100644
index 6855853..0000000
--- a/components/zucchini/reference_bytes_mixer.cc
+++ /dev/null
@@ -1,150 +0,0 @@
-// Copyright 2018 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 "components/zucchini/reference_bytes_mixer.h"
-
-#include <algorithm>
-
-#include "base/check_op.h"
-#include "base/logging.h"
-#include "base/notreached.h"
-#include "components/zucchini/disassembler.h"
-#include "components/zucchini/disassembler_elf.h"
-
-namespace zucchini {
-
-/******** ReferenceBytesMixer ********/
-
-// Default implementation is a stub, i.e., for architectures whose references
-// have operation bits and payload bits stored in separate bytes. So during
-// patch application, payload bits are copied for matched blocks, ignored by
-// bytewise corrections, and fixed by reference target corrections.
-ReferenceBytesMixer::ReferenceBytesMixer() {}
-
-ReferenceBytesMixer::~ReferenceBytesMixer() = default;
-
-// static.
-std::unique_ptr<ReferenceBytesMixer> ReferenceBytesMixer::Create(
-    const Disassembler& src_dis,
-    const Disassembler& dst_dis) {
-  ExecutableType exe_type = src_dis.GetExeType();
-  DCHECK_EQ(exe_type, dst_dis.GetExeType());
-  if (exe_type == kExeTypeElfAArch32)
-    return std::make_unique<ReferenceBytesMixerElfArm>(exe_type);
-  if (exe_type == kExeTypeElfAArch64)
-    return std::make_unique<ReferenceBytesMixerElfArm>(exe_type);
-  return std::make_unique<ReferenceBytesMixer>();
-}
-
-// Stub implementation.
-int ReferenceBytesMixer::NumBytes(uint8_t type) const {
-  return 0;
-}
-
-// Base class implementation is a stub that should not be called.
-ConstBufferView ReferenceBytesMixer::Mix(uint8_t type,
-                                         ConstBufferView old_view,
-                                         offset_t old_offset,
-                                         ConstBufferView new_view,
-                                         offset_t new_offset) {
-  NOTREACHED() << "Stub.";
-  return ConstBufferView();
-}
-
-/******** ReferenceBytesMixerElfArm ********/
-
-ReferenceBytesMixerElfArm::ReferenceBytesMixerElfArm(ExecutableType exe_type)
-    : exe_type_(exe_type), out_buffer_(4) {}  // 4 is a bound on NumBytes().
-
-ReferenceBytesMixerElfArm::~ReferenceBytesMixerElfArm() = default;
-
-int ReferenceBytesMixerElfArm::NumBytes(uint8_t type) const {
-  if (exe_type_ == kExeTypeElfAArch32) {
-    switch (type) {
-      case AArch32ReferenceType::kRel32_A24:  // Falls through.
-      case AArch32ReferenceType::kRel32_T20:
-      case AArch32ReferenceType::kRel32_T24:
-        return 4;
-      case AArch32ReferenceType::kRel32_T8:  // Falls through.
-      case AArch32ReferenceType::kRel32_T11:
-        return 2;
-    }
-  } else if (exe_type_ == kExeTypeElfAArch64) {
-    switch (type) {
-      case AArch64ReferenceType::kRel32_Immd14:  // Falls through.
-      case AArch64ReferenceType::kRel32_Immd19:
-      case AArch64ReferenceType::kRel32_Immd26:
-        return 4;
-    }
-  }
-  return 0;
-}
-
-ConstBufferView ReferenceBytesMixerElfArm::Mix(uint8_t type,
-                                               ConstBufferView old_view,
-                                               offset_t old_offset,
-                                               ConstBufferView new_view,
-                                               offset_t new_offset) {
-  int num_bytes = NumBytes(type);
-  ConstBufferView::const_iterator new_it = new_view.begin() + new_offset;
-  DCHECK_LE(static_cast<size_t>(num_bytes), out_buffer_.size());
-  MutableBufferView out_buffer_view(&out_buffer_[0], num_bytes);
-  std::copy(new_it, new_it + num_bytes, out_buffer_view.begin());
-
-  ArmCopyDispFun copier = GetCopier(type);
-  DCHECK_NE(copier, nullptr);
-
-  if (!copier(old_view, old_offset, out_buffer_view, 0U)) {
-    // Failed to mix old payload bits with new operation bits. The main cause of
-    // of this rare failure is when BL (encoding T1) with payload bits
-    // representing disp % 4 == 2 transforms into BLX (encoding T2). Error
-    // arises because BLX requires payload bits to have disp == 0 (mod 4).
-    // Mixing failures are not fatal to patching; we simply fall back to direct
-    // copy and forgo benefits from mixing for these cases.
-    // TODO(huangs, etiennep): Ongoing discussion on whether we should just
-    // nullify all payload disp so we won't have to deal with this case, but at
-    // the cost of having Zucchini-apply do more work.
-    static int output_quota = 10;
-    if (output_quota > 0) {
-      LOG(WARNING) << "Reference byte mix failed with type = "
-                   << static_cast<uint32_t>(type) << "." << std::endl;
-      --output_quota;
-      if (!output_quota)
-        LOG(WARNING) << "(Additional output suppressed)";
-    }
-    // Fall back to direct copy.
-    std::copy(new_it, new_it + num_bytes, out_buffer_view.begin());
-  }
-  return ConstBufferView(out_buffer_view);
-}
-
-ArmCopyDispFun ReferenceBytesMixerElfArm::GetCopier(uint8_t type) const {
-  if (exe_type_ == kExeTypeElfAArch32) {
-    switch (type) {
-      case AArch32ReferenceType::kRel32_A24:
-        return ArmCopyDisp<AArch32Rel32Translator::AddrTraits_A24>;
-      case AArch32ReferenceType::kRel32_T8:
-        return ArmCopyDisp<AArch32Rel32Translator::AddrTraits_T8>;
-      case AArch32ReferenceType::kRel32_T11:
-        return ArmCopyDisp<AArch32Rel32Translator::AddrTraits_T11>;
-      case AArch32ReferenceType::kRel32_T20:
-        return ArmCopyDisp<AArch32Rel32Translator::AddrTraits_T20>;
-      case AArch32ReferenceType::kRel32_T24:
-        return ArmCopyDisp<AArch32Rel32Translator::AddrTraits_T24>;
-    }
-  } else if (exe_type_ == kExeTypeElfAArch64) {
-    switch (type) {
-      case AArch64ReferenceType::kRel32_Immd14:
-        return ArmCopyDisp<AArch64Rel32Translator::AddrTraits_Immd14>;
-      case AArch64ReferenceType::kRel32_Immd19:
-        return ArmCopyDisp<AArch64Rel32Translator::AddrTraits_Immd19>;
-      case AArch64ReferenceType::kRel32_Immd26:
-        return ArmCopyDisp<AArch64Rel32Translator::AddrTraits_Immd26>;
-    }
-  }
-  DLOG(FATAL) << "NOTREACHED";
-  return nullptr;
-}
-
-}  // namespace zucchini
diff --git a/components/zucchini/reference_bytes_mixer.h b/components/zucchini/reference_bytes_mixer.h
deleted file mode 100644
index f20b0ef9..0000000
--- a/components/zucchini/reference_bytes_mixer.h
+++ /dev/null
@@ -1,118 +0,0 @@
-// Copyright 2018 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 COMPONENTS_ZUCCHINI_REFERENCE_BYTES_MIXER_H_
-#define COMPONENTS_ZUCCHINI_REFERENCE_BYTES_MIXER_H_
-
-#include <stdint.h>
-
-#include <memory>
-
-#include "components/zucchini/buffer_view.h"
-#include "components/zucchini/image_utils.h"
-#include "components/zucchini/rel32_utils.h"
-
-namespace zucchini {
-
-class Disassembler;
-
-// References encoding may be quite complex in some architectures (e.g., ARM),
-// requiring bit-level manipulation. In general, bits in a reference body fall
-// under 2 categories:
-// - Operation bits: Instruction op code, conditionals, or structural data.
-// - Payload bits: Actual target data of the reference. These may be absolute,
-//   or be displacements relative to instruction pointer / program counter.
-// During patch application,
-//   Old reference bytes = {old operation, old payload},
-// is transformed to
-//   New reference bytes = {new operation, new payload}.
-// New image bytes are written by three sources:
-//   (1) Direct copy from old image to new image for matched blocks.
-//   (2) Bytewise diff correction.
-//   (3) Dedicated reference target correction.
-//
-// For references whose operation and payload bits are stored in easily
-// separable bytes (e.g., rel32 reference in X86), (2) can exclude payload bits.
-// So during patch application, (1) naively copies everything, (2) fixes
-// operation bytes only, and (3) fixes payload bytes only.
-//
-// For architectures with references whose operation and payload bits may mix
-// within shared bytes (e.g., ARM rel32), a dilemma arises:
-// - (2) cannot ignores shared bytes, since otherwise new operation bits not
-//   properly transfer.
-// - Having (2) always overwrite these bytes would reduce the benefits of
-//   reference correction, since references are likely to change.
-//
-// Our solution applies a hybrid approach: For each matching old / new reference
-// pair, define:
-//   Mixed reference bytes = {new operation, old payload},
-//
-// During patch generation, we compute bytewise correction from old reference
-// bytes to the mixed reference bytes. So during patch application, (2) only
-// corrects operation bit changes (and skips if they don't change), and (3)
-// overwrites old payload bits to new payload bits.
-
-// A base class for (stateful) mixed reference byte generation. This base class
-// serves as a stub. Architectures whose references store operation bits and
-// payload bits can share common bytes (e.g., ARM rel32) should override this.
-class ReferenceBytesMixer {
- public:
-  ReferenceBytesMixer();
-  ReferenceBytesMixer(const ReferenceBytesMixer&) = delete;
-  const ReferenceBytesMixer& operator=(const ReferenceBytesMixer&) = delete;
-  virtual ~ReferenceBytesMixer();
-
-  // Returns a new ReferenceBytesMixer instance that's owned by the caller.
-  static std::unique_ptr<ReferenceBytesMixer> Create(
-      const Disassembler& src_dis,
-      const Disassembler& dst_dis);
-
-  // Returns the number of bytes that need to be mixed for references with given
-  // |type|. Returns 0 if no mixing is required.
-  virtual int NumBytes(uint8_t type) const;
-
-  // Computes mixed reference bytes by combining (a) "payload bits" from an
-  // "old" reference of |type| at |old_view[old_offset]| with (b) "operation
-  // bits" from a "new" reference of |type| at |new_view[new_offset]|. Returns
-  // the result as ConstBufferView, which is valid only until the next call to
-  // Mix().
-  virtual ConstBufferView Mix(uint8_t type,
-                              ConstBufferView old_view,
-                              offset_t old_offset,
-                              ConstBufferView new_view,
-                              offset_t new_offset);
-};
-
-// In AArch32 and AArch64, instructions mix operation bits and payload bits in
-// complex ways. This is the main use case of ReferenceBytesMixer.
-class ReferenceBytesMixerElfArm : public ReferenceBytesMixer {
- public:
-  // |exe_type| must be EXE_TYPE_ELF_ARM or EXE_TYPE_ELF_AARCH64.
-  explicit ReferenceBytesMixerElfArm(ExecutableType exe_type);
-  ReferenceBytesMixerElfArm(const ReferenceBytesMixerElfArm&) = delete;
-  const ReferenceBytesMixerElfArm& operator=(const ReferenceBytesMixerElfArm&) =
-      delete;
-  ~ReferenceBytesMixerElfArm() override;
-
-  // ReferenceBytesMixer:
-  int NumBytes(uint8_t type) const override;
-  ConstBufferView Mix(uint8_t type,
-                      ConstBufferView old_view,
-                      offset_t old_offset,
-                      ConstBufferView new_view,
-                      offset_t new_offset) override;
-
- private:
-  ArmCopyDispFun GetCopier(uint8_t type) const;
-
-  // For simplicity, 32-bit vs. 64-bit distinction is represented by state
-  // |exe_type_|, instead of creating derived classes.
-  const ExecutableType exe_type_;
-
-  std::vector<uint8_t> out_buffer_;
-};
-
-}  // namespace zucchini
-
-#endif  // COMPONENTS_ZUCCHINI_REFERENCE_BYTES_MIXER_H_
diff --git a/components/zucchini/rel32_utils.cc b/components/zucchini/rel32_utils.cc
index c22cb23..0ede84f 100644
--- a/components/zucchini/rel32_utils.cc
+++ b/components/zucchini/rel32_utils.cc
@@ -64,4 +64,23 @@
   image_.write<uint32_t>(ref.location, code);
 }
 
+void OutputArmCopyDispFailure(uint32_t addr_type) {
+  // Failed to mix old payload bits with new operation bits. The main cause of
+  // this rare failure is when BL (encoding T1) with payload bits representing
+  // disp % 4 == 2 transforms into BLX (encoding T2). Error arises because BLX
+  // requires payload bits to have disp == 0 (mod 4). Mixing failures are not
+  // fatal to patching; we simply fall back to direct copy and forgo benefits
+  // from mixing for these cases. TODO(huangs, etiennep): Ongoing discussion on
+  // whether we should just nullify all payload disp so we won't have to deal
+  // with this case, but at the cost of having Zucchini-apply do more work.
+  static int output_quota = 10;
+  if (output_quota > 0) {
+    LOG(WARNING) << "Reference byte mix failed with type = " << addr_type << "."
+                 << std::endl;
+    --output_quota;
+    if (!output_quota)
+      LOG(WARNING) << "(Additional output suppressed)";
+  }
+}
+
 }  // namespace zucchini
diff --git a/components/zucchini/rel32_utils.h b/components/zucchini/rel32_utils.h
index f54c5cd..10921d0 100644
--- a/components/zucchini/rel32_utils.h
+++ b/components/zucchini/rel32_utils.h
@@ -6,6 +6,7 @@
 #define COMPONENTS_ZUCCHINI_REL32_UTILS_H_
 
 #include <algorithm>
+#include <array>
 #include <deque>
 #include <memory>
 
@@ -148,14 +149,6 @@
   AddressTranslator::OffsetToRvaCache offset_to_rva_;
 };
 
-// Type for specialized versions of ArmCopyDisp().
-// TODO(etiennep/huangs): Fold ReferenceByteMixer into Disassembler and remove
-//     direct function pointer usage.
-using ArmCopyDispFun = bool (*)(ConstBufferView src_view,
-                                offset_t src_idx,
-                                MutableBufferView dst_view,
-                                offset_t dst_idx);
-
 // Copier that makes |*dst_it| similar to |*src_it| (both assumed to point to
 // rel32 instructions of type ADDR_TRAITS) by copying the displacement (i.e.,
 // payload bits) from |src_it| to |dst_it|. If successful, updates |*dst_it|,
@@ -179,6 +172,41 @@
   return false;
 }
 
+// Outputs error message (throttled) on ArmCopyDisp failure.
+void OutputArmCopyDispFailure(uint32_t addr_type);
+
+// Mixer for ARM rel32 References of a specific type.
+template <class ADDR_TRAITS>
+class Rel32MixerArm : public ReferenceMixer {
+  using code_t = typename ADDR_TRAITS::code_t;
+  static constexpr size_t kCodeWidth = sizeof(code_t);
+
+ public:
+  Rel32MixerArm(ConstBufferView src_image, ConstBufferView dst_image)
+      : src_image_(src_image), dst_image_(dst_image) {}
+  ~Rel32MixerArm() override = default;
+
+  ConstBufferView Mix(offset_t src_offset, offset_t dst_offset) override {
+    ConstBufferView::const_iterator new_it = dst_image_.begin() + dst_offset;
+    MutableBufferView out_buffer_view(out_buffer_.data(), kCodeWidth);
+    std::copy(new_it, new_it + kCodeWidth, out_buffer_view.begin());
+
+    if (!ArmCopyDisp<ADDR_TRAITS>(src_image_, src_offset, out_buffer_view,
+                                  0U)) {
+      OutputArmCopyDispFailure(static_cast<uint32_t>(ADDR_TRAITS::addr_type));
+      // Fall back to direct copy.
+      std::copy(new_it, new_it + kCodeWidth, out_buffer_.begin());
+    }
+    return ConstBufferView(out_buffer_.data(), kCodeWidth);
+  }
+
+ private:
+  ConstBufferView src_image_;
+  ConstBufferView dst_image_;
+
+  std::array<uint8_t, sizeof(code_t)> out_buffer_;
+};
+
 }  // namespace zucchini
 
 #endif  // COMPONENTS_ZUCCHINI_REL32_UTILS_H_
diff --git a/components/zucchini/rel32_utils_unittest.cc b/components/zucchini/rel32_utils_unittest.cc
index f4a6bde..e122680 100644
--- a/components/zucchini/rel32_utils_unittest.cc
+++ b/components/zucchini/rel32_utils_unittest.cc
@@ -43,6 +43,11 @@
   EXPECT_EQ(absl::nullopt, reader->GetNext());  // Nothing should be left.
 }
 
+using ArmCopyDispFun = bool (*)(ConstBufferView src_view,
+                                offset_t src_idx,
+                                MutableBufferView dst_view,
+                                offset_t dst_idx);
+
 // Copies displacements from |bytes1| to |bytes2| and checks results against
 // |bytes_exp_1_to_2|. Then repeats for |*bytes2| , |*byte1|, and
 // |bytes_exp_2_to_1|. Empty expected bytes mean failure is expected. The copy
diff --git a/components/zucchini/zucchini_gen.cc b/components/zucchini/zucchini_gen.cc
index 3735d0f..45b1f72 100644
--- a/components/zucchini/zucchini_gen.cc
+++ b/components/zucchini/zucchini_gen.cc
@@ -24,7 +24,6 @@
 #include "components/zucchini/image_index.h"
 #include "components/zucchini/imposed_ensemble_matcher.h"
 #include "components/zucchini/patch_writer.h"
-#include "components/zucchini/reference_bytes_mixer.h"
 #include "components/zucchini/suffix_array.h"
 #include "components/zucchini/targets_affinity.h"
 
@@ -119,12 +118,13 @@
   return true;
 }
 
-bool GenerateRawDelta(ConstBufferView old_image,
-                      ConstBufferView new_image,
-                      const EquivalenceMap& equivalence_map,
-                      const ImageIndex& new_image_index,
-                      ReferenceBytesMixer* reference_bytes_mixer,
-                      PatchElementWriter* patch_writer) {
+bool GenerateRawDelta(
+    ConstBufferView old_image,
+    ConstBufferView new_image,
+    const EquivalenceMap& equivalence_map,
+    const ImageIndex& new_image_index,
+    const std::map<TypeTag, std::unique_ptr<ReferenceMixer>>& reference_mixers,
+    PatchElementWriter* patch_writer) {
   RawDeltaSink raw_delta_sink;
 
   // Visit |equivalence_map| blocks in |new_image| order. Find and emit all
@@ -139,24 +139,24 @@
         DCHECK(new_image_index.IsToken(equivalence.dst_offset + i));
         TypeTag type_tag =
             new_image_index.LookupType(equivalence.dst_offset + i);
+        ReferenceMixer* mixer = reference_mixers.at(type_tag).get();
+        offset_t width = new_image_index.refs(type_tag).width();
 
         // Reference delta has its own flow. On some architectures (e.g., x86)
         // this does not involve raw delta, so we skip. On other architectures
         // (e.g., ARM) references are mixed with other bits that may change, so
         // we need to "mix" data and store some changed bits into raw delta.
-        int num_bytes = reference_bytes_mixer->NumBytes(type_tag.value());
-        if (num_bytes) {
-          ConstBufferView mixed_ref_bytes = reference_bytes_mixer->Mix(
-              type_tag.value(), old_image, equivalence.src_offset + i,
-              new_image, equivalence.dst_offset + i);
-          for (int j = 0; j < num_bytes; ++j) {
+        if (mixer) {
+          ConstBufferView mixed_reference = mixer->Mix(
+              equivalence.src_offset + i, equivalence.dst_offset + i);
+          for (offset_t j = 0; j < width; ++j) {
             int8_t diff =
-                mixed_ref_bytes[j] - old_image[equivalence.src_offset + i + j];
-            if (diff)
+                mixed_reference[j] - old_image[equivalence.src_offset + i + j];
+            if (diff != 0)
               raw_delta_sink.PutNext({base_copy_offset + i + j, diff});
           }
         }
-        i += new_image_index.refs(type_tag).width();
+        i += width;
         DCHECK_LE(i, equivalence.length);
       } else {
         int8_t diff = new_image[equivalence.dst_offset + i] -
@@ -252,11 +252,11 @@
 
   patch_writer->SetReferenceDeltaSink({});
 
-  ReferenceBytesMixer no_op_bytes_mixer;
+  std::map<TypeTag, std::unique_ptr<ReferenceMixer>> reference_mixers;
   return GenerateEquivalencesAndExtraData(new_image, equivalences,
                                           patch_writer) &&
          GenerateRawDelta(old_image, new_image, equivalences, new_image_index,
-                          &no_op_bytes_mixer, patch_writer);
+                          reference_mixers, patch_writer);
 }
 
 bool GenerateExecutableElement(ExecutableType exe_type,
@@ -311,13 +311,20 @@
       }
     }
   }
+  std::map<TypeTag, std::unique_ptr<ReferenceMixer>> reference_mixers;
+  std::vector<ReferenceGroup> ref_groups = old_disasm->MakeReferenceGroups();
+  for (const auto& group : ref_groups) {
+    auto result = reference_mixers.emplace(
+        group.type_tag(),
+        group.GetMixer(old_image, new_image, old_disasm.get()));
+    DCHECK(result.second);
+  }
+
   patch_writer->SetReferenceDeltaSink(std::move(reference_delta_sink));
-  std::unique_ptr<ReferenceBytesMixer> reference_bytes_mixer =
-      ReferenceBytesMixer::Create(*old_disasm, *new_disasm);
   return GenerateEquivalencesAndExtraData(new_image, equivalences,
                                           patch_writer) &&
          GenerateRawDelta(old_image, new_image, equivalences, new_image_index,
-                          reference_bytes_mixer.get(), patch_writer);
+                          reference_mixers, patch_writer);
 }
 
 status::Code GenerateBufferCommon(ConstBufferView old_image,
diff --git a/components/zucchini/zucchini_gen.h b/components/zucchini/zucchini_gen.h
index ac28263..84e2f4b6 100644
--- a/components/zucchini/zucchini_gen.h
+++ b/components/zucchini/zucchini_gen.h
@@ -17,7 +17,6 @@
 class OffsetMapper;
 class ImageIndex;
 class PatchElementWriter;
-class ReferenceBytesMixer;
 class ReferenceDeltaSink;
 class ReferenceSet;
 class TargetPool;
@@ -44,12 +43,13 @@
 // Writes raw delta between |old_image| and |new_image| matched by
 // |equivalence_map| to |patch_writer|, using |new_image_index| to ignore
 // reference bytes.
-bool GenerateRawDelta(ConstBufferView old_image,
-                      ConstBufferView new_image,
-                      const EquivalenceMap& equivalence_map,
-                      const ImageIndex& new_image_index,
-                      ReferenceBytesMixer* reference_bytes_mixer,
-                      PatchElementWriter* patch_writer);
+bool GenerateRawDelta(
+    ConstBufferView old_image,
+    ConstBufferView new_image,
+    const EquivalenceMap& equivalence_map,
+    const ImageIndex& new_image_index,
+    const std::map<TypeTag, std::unique_ptr<ReferenceMixer>>& reference_mixers,
+    PatchElementWriter* patch_writer);
 
 // Writes reference delta between references from |old_refs| and from
 // |new_refs| to |patch_writer|. |projected_target_pool| contains projected
diff --git a/content/browser/bad_message.h b/content/browser/bad_message.h
index 3209677..f8ea8ad6 100644
--- a/content/browser/bad_message.h
+++ b/content/browser/bad_message.h
@@ -297,6 +297,7 @@
   RFH_BEFOREUNLOAD_HANDLER_NOT_ALLOWED_IN_FENCED_FRAME = 270,
   MSDH_GET_OPEN_DEVICE_USE_WITHOUT_FEATURE = 271,
   RFHI_SUBFRAME_NAV_WOULD_CHANGE_MAINFRAME_ORIGIN = 272,
+  FF_CREATE_WHILE_PRERENDERING = 273,
 
   // Please add new elements here. The naming convention is abbreviated class
   // name (e.g. RenderFrameHost becomes RFH) plus a unique description of the
diff --git a/content/browser/fenced_frame/fenced_frame.cc b/content/browser/fenced_frame/fenced_frame.cc
index cbf337b..b75bd48d 100644
--- a/content/browser/fenced_frame/fenced_frame.cc
+++ b/content/browser/fenced_frame/fenced_frame.cc
@@ -34,7 +34,8 @@
 }  // namespace
 
 FencedFrame::FencedFrame(
-    base::SafeRef<RenderFrameHostImpl> owner_render_frame_host)
+    base::SafeRef<RenderFrameHostImpl> owner_render_frame_host,
+    blink::mojom::FencedFrameMode mode)
     : web_contents_(static_cast<WebContentsImpl*>(
           WebContents::FromRenderFrameHost(&*owner_render_frame_host))),
       owner_render_frame_host_(owner_render_frame_host),
@@ -50,7 +51,8 @@
                                       /*render_widget_delegate=*/web_contents_,
                                       /*manager_delegate=*/web_contents_,
                                       /*page_delegate=*/web_contents_,
-                                      FrameTree::Type::kFencedFrame)) {
+                                      FrameTree::Type::kFencedFrame)),
+      mode_(mode) {
   scoped_refptr<SiteInstance> site_instance =
       SiteInstance::Create(web_contents_->GetBrowserContext());
   // Note that even though this is happening in response to an event in the
@@ -84,6 +86,11 @@
 
 void FencedFrame::Navigate(const GURL& url,
                            base::TimeTicks navigation_start_time) {
+  // We don't need guard against a bad message in the case of prerendering since
+  // we wouldn't even establish the mojo connection in that case.
+  DCHECK_NE(RenderFrameHost::LifecycleState::kPrerendering,
+            owner_render_frame_host_->GetLifecycleState());
+
   FrameTreeNode* inner_root = frame_tree_->root();
 
   // TODO(crbug.com/1237552): Resolve the discussion around navigations being
diff --git a/content/browser/fenced_frame/fenced_frame.h b/content/browser/fenced_frame/fenced_frame.h
index a046c9c..83cba30f 100644
--- a/content/browser/fenced_frame/fenced_frame.h
+++ b/content/browser/fenced_frame/fenced_frame.h
@@ -35,7 +35,8 @@
                                    public NavigationControllerDelegate {
  public:
   explicit FencedFrame(
-      base::SafeRef<RenderFrameHostImpl> owner_render_frame_host);
+      base::SafeRef<RenderFrameHostImpl> owner_render_frame_host,
+      blink::mojom::FencedFrameMode mode);
   ~FencedFrame() override;
 
   void Bind(mojo::PendingAssociatedReceiver<blink::mojom::FencedFrameOwnerHost>
@@ -66,6 +67,8 @@
 
   RenderFrameHostImpl* GetInnerRoot() { return frame_tree_->GetMainFrame(); }
 
+  blink::mojom::FencedFrameMode mode() const { return mode_; }
+
  private:
   // NavigationControllerDelegate
   void NotifyNavigationStateChanged(InvalidateTypes changed_flags) override;
@@ -113,6 +116,13 @@
   // The FrameTree that we create to host the "inner" fenced frame contents.
   std::unique_ptr<FrameTree> frame_tree_;
 
+  // The `mode` attribute set on the fenced frame. The mode will stay the same
+  // across navigations to avoid privacy leak. Since each mode might have
+  // different access constraints, privacy leak might occur if the mode is
+  // mutable as a fenced frame can pass the information it learned in one mode
+  // to the other mode if mode was changed across navigations.
+  const blink::mojom::FencedFrameMode mode_;
+
   // Receives messages from the frame owner element in Blink.
   mojo::AssociatedReceiver<blink::mojom::FencedFrameOwnerHost> receiver_{this};
 };
diff --git a/content/browser/fenced_frame/fenced_frame_browsertest.cc b/content/browser/fenced_frame/fenced_frame_browsertest.cc
index 244c05b..fd5d5c05 100644
--- a/content/browser/fenced_frame/fenced_frame_browsertest.cc
+++ b/content/browser/fenced_frame/fenced_frame_browsertest.cc
@@ -265,12 +265,13 @@
   void CreateFencedFrame(
       mojo::PendingAssociatedReceiver<blink::mojom::FencedFrameOwnerHost>
           pending_receiver,
+      blink::mojom::FencedFrameMode mode,
       CreateFencedFrameCallback callback) override {
     mojo::PendingAssociatedRemote<blink::mojom::FencedFrameOwnerHost>
         original_remote;
 
     GetForwardingInterface()->CreateFencedFrame(
-        original_remote.InitWithNewEndpointAndPassReceiver(),
+        original_remote.InitWithNewEndpointAndPassReceiver(), mode,
         std::move(callback));
     std::vector<FencedFrame*> fenced_frames =
         render_frame_host_->GetFencedFrames();
diff --git a/content/browser/prerender/prerender_browsertest.cc b/content/browser/prerender/prerender_browsertest.cc
index 3c60f8d..00b9d21 100644
--- a/content/browser/prerender/prerender_browsertest.cc
+++ b/content/browser/prerender/prerender_browsertest.cc
@@ -5895,4 +5895,65 @@
   NavigatePrimaryPage(kPrerenderingUrl);
 }
 
+class PrerenderFencedFrameBrowserTest
+    : public PrerenderBrowserTest,
+      public testing::WithParamInterface<bool /* shadow_dom_fenced_frames */> {
+ public:
+  PrerenderFencedFrameBrowserTest() {
+    feature_list_.InitAndEnableFeatureWithParameters(
+        blink::features::kFencedFrames,
+        {{"implementation_type", GetParam() ? "shadow_dom" : "mparch"}});
+  }
+  ~PrerenderFencedFrameBrowserTest() override = default;
+
+  bool IsShadowDomImpl() const { return GetParam(); }
+
+ private:
+  base::test::ScopedFeatureList feature_list_;
+};
+
+IN_PROC_BROWSER_TEST_P(PrerenderFencedFrameBrowserTest,
+                       PrerenderFencedFrameBrowserTest) {
+  const GURL kInitialUrl = GetUrl("/empty.html");
+  const GURL kPrerenderingUrl = GetUrl("/empty.html?prerender");
+  const GURL kFencedFrameUrl = GetUrl("/title1.html");
+  constexpr char kAddFencedFrameScript[] = R"({
+    const fenced_frame = document.createElement('fencedframe');
+    fenced_frame.src = $1;
+    document.body.appendChild(fenced_frame);
+  })";
+
+  // We see a navigation to about:blank for Shadow DOM, but not MPArch, so we
+  // need to account for another navigation with that implementation.
+  const int kNumNavigations = IsShadowDomImpl() ? 4 : 3;
+  TestNavigationObserver nav_observer(web_contents(), kNumNavigations);
+
+  ASSERT_TRUE(NavigateToURL(shell(), kInitialUrl));
+
+  // Start a prerender.
+  int host_id = AddPrerender(kPrerenderingUrl);
+  auto* prerendered_rfh = GetPrerenderedMainFrameHost(host_id);
+  EXPECT_EQ(kPrerenderingUrl, nav_observer.last_navigation_url());
+  EXPECT_TRUE(ExecJs(prerendered_rfh,
+                     JsReplace(kAddFencedFrameScript, kFencedFrameUrl)));
+  // Since we've deferred creating the fenced frame delegate, we should see no
+  // child frames.
+  size_t child_frame_count = 0;
+  prerendered_rfh->ForEachRenderFrameHost(
+      base::BindLambdaForTesting([&](RenderFrameHostImpl* rfh) {
+        if (rfh != prerendered_rfh)
+          child_frame_count++;
+      }));
+  EXPECT_EQ(0lu, child_frame_count);
+
+  NavigatePrimaryPage(kPrerenderingUrl);
+  EXPECT_EQ(kPrerenderingUrl, nav_observer.last_navigation_url());
+  nav_observer.Wait();
+  EXPECT_EQ(kFencedFrameUrl, nav_observer.last_navigation_url());
+}
+
+INSTANTIATE_TEST_SUITE_P(PrerenderFencedFrameBrowserTest,
+                         PrerenderFencedFrameBrowserTest,
+                         testing::Bool());
+
 }  // namespace content
diff --git a/content/browser/renderer_host/frame_tree_browsertest.cc b/content/browser/renderer_host/frame_tree_browsertest.cc
index 233a0b1..8294b79e 100644
--- a/content/browser/renderer_host/frame_tree_browsertest.cc
+++ b/content/browser/renderer_host/frame_tree_browsertest.cc
@@ -2216,6 +2216,7 @@
 
     EXPECT_TRUE(ExecJs(root,
                        "var f = document.createElement('fencedframe');"
+                       "f.mode = 'opaque-ads';"
                        "document.body.appendChild(f);"));
 
     EXPECT_EQ(1U, root->child_count());
@@ -2241,11 +2242,11 @@
     if (!test_case.expect_allowed)
       EXPECT_EQ("fenced-frame-src;", EvalJs(root, "violation"));
 
-    absl::optional<FrameTreeNode::FencedFrameMode> fenced_frame_mode =
-        fenced_frame_root_node->fenced_frame_mode();
+    absl::optional<blink::mojom::FencedFrameMode> fenced_frame_mode =
+        fenced_frame_root_node->GetFencedFrameMode();
     EXPECT_TRUE(fenced_frame_mode.has_value());
     EXPECT_EQ(fenced_frame_mode.value(),
-              FrameTreeNode::FencedFrameMode::kOpaque);
+              blink::mojom::FencedFrameMode::kOpaqueAds);
   }
 }
 
diff --git a/content/browser/renderer_host/frame_tree_node.cc b/content/browser/renderer_host/frame_tree_node.cc
index 6d6c142..ac13436 100644
--- a/content/browser/renderer_host/frame_tree_node.cc
+++ b/content/browser/renderer_host/frame_tree_node.cc
@@ -48,6 +48,23 @@
 base::LazyInstance<FrameTreeNodeIdMap>::DestructorAtExit
     g_frame_tree_node_id_map = LAZY_INSTANCE_INITIALIZER;
 
+FencedFrame* FindFencedFrame(const FrameTreeNode* frame_tree_node) {
+  // TODO(crbug.com/1123606): Consider having a pointer to `FencedFrame` in
+  // `FrameTreeNode` or having a map between them.
+
+  // Try and find the `FencedFrame` that `frame_tree_node` represents.
+  DCHECK(frame_tree_node->parent());
+  std::vector<FencedFrame*> fenced_frames =
+      frame_tree_node->parent()->GetFencedFrames();
+  for (FencedFrame* fenced_frame : fenced_frames) {
+    if (frame_tree_node->frame_tree_node_id() ==
+        fenced_frame->GetOuterDelegateFrameTreeNodeId()) {
+      return fenced_frame;
+    }
+  }
+  return nullptr;
+}
+
 }  // namespace
 
 // This observer watches the opener of its owner FrameTreeNode and clears the
@@ -151,18 +168,7 @@
   }
 
   if (is_outer_dummy_node) {
-    DCHECK(parent());
-    // Try and find the `FencedFrame` that `this` represents.
-    std::vector<FencedFrame*> fenced_frames = parent()->GetFencedFrames();
-    FencedFrame* doomed_fenced_frame = nullptr;
-    for (FencedFrame* fenced_frame : fenced_frames) {
-      if (frame_tree_node_id() ==
-          fenced_frame->GetOuterDelegateFrameTreeNodeId()) {
-        doomed_fenced_frame = fenced_frame;
-        break;
-      }
-    }
-
+    FencedFrame* doomed_fenced_frame = FindFencedFrame(this);
     // `doomed_fenced_frame` might not actually exist, because some outer dummy
     // `FrameTreeNode`s might correspond to `Portal`s, which do not have their
     // lifetime managed in the same way as `FencedFrames`.
@@ -843,16 +849,21 @@
   fenced_frame_nonce_ = nonce;
 }
 
-void FrameTreeNode::SetFencedFrameModeIfNeeded(
-    FencedFrameMode fenced_frame_mode) {
+absl::optional<blink::mojom::FencedFrameMode>
+FrameTreeNode::GetFencedFrameMode() {
   if (!IsFencedFrameRoot())
-    return;
+    return absl::nullopt;
 
-  // TODO(crbug.com/1123606): The 'mode' attribute cannot be changed once
-  // applied to a fenced frame. This will be enforced before this point so add
-  // a DCHECK here.
+  if (blink::features::IsFencedFramesShadowDOMBased())
+    return pending_frame_policy_.fenced_frame_mode;
 
-  fenced_frame_mode_ = fenced_frame_mode;
+  FrameTreeNode* outer_delegate = render_manager()->GetOuterDelegateNode();
+  DCHECK(outer_delegate);
+
+  FencedFrame* fenced_frame = FindFencedFrame(outer_delegate);
+  DCHECK(fenced_frame);
+
+  return fenced_frame->mode();
 }
 
 bool FrameTreeNode::IsErrorPageIsolationEnabled() const {
diff --git a/content/browser/renderer_host/frame_tree_node.h b/content/browser/renderer_host/frame_tree_node.h
index 93c771229..d73a443de 100644
--- a/content/browser/renderer_host/frame_tree_node.h
+++ b/content/browser/renderer_host/frame_tree_node.h
@@ -67,15 +67,6 @@
     virtual ~Observer() = default;
   };
 
-  // Indicates whether the fenced frame url is opaque or not.
-  //
-  // TODO(https://crbug.com/1123606): Revisit where to define the mode when the
-  // 'mode' attribute is introduced.
-  enum class FencedFrameMode {
-    kOpaque,
-    kDefault,
-  };
-
   static const int kFrameTreeNodeInvalidId;
 
   // Returns the FrameTreeNode with the given global |frame_tree_node_id|,
@@ -496,16 +487,9 @@
   // by FrameTree::Init() or FrameTree::AddFrame().
   void SetFencedFrameNonceIfNeeded();
 
-  // Returns the fenced frame mode if `IsFencedFrameRoot()` returns true for
-  // `this`. Returns nullopt otherwise. See comments on `fenced_frame_mode_` for
-  // more details.
-  absl::optional<FencedFrameMode> fenced_frame_mode() {
-    return fenced_frame_mode_;
-  }
-
-  // If applicable, set the fenced frame mode if it's not been set yet. Invoked
-  // by `NavigationRequest::BeginNavigation()`.
-  void SetFencedFrameModeIfNeeded(FencedFrameMode fenced_frame_mode);
+  // Returns the mode attribute set on the fenced frame if this is a fenced
+  // frame root, otherwise returns `absl::nullopt`.
+  absl::optional<blink::mojom::FencedFrameMode> GetFencedFrameMode();
 
   // Helper for GetParentOrOuterDocument/GetParentOrOuterDocumentOrEmbedder.
   // Do not use directly.
@@ -728,17 +712,6 @@
   // partition will be used.
   absl::optional<base::UnguessableToken> fenced_frame_nonce_;
 
-  // Fenced Frames:
-  // Indicates whether the fenced frame is navigated to a urn:uuid or not. Not
-  // set if this frame is not fenced frame or it is a fenced frame but before
-  // `NavigationRequest::BeginNavigation()` is called which implicitly sets the
-  // mode. The mode will stay the same across navigations to avoid privacy leak.
-  // Since each mode might have different access constraints, privacy leak might
-  // occur if the mode is mutable as a fenced frame can pass the information it
-  // learned in one mode to the other mode if mode was changed across
-  // navigations.
-  absl::optional<FencedFrameMode> fenced_frame_mode_;
-
   // Manages creation and swapping of RenderFrameHosts for this frame.
   //
   // This field needs to be declared last, because destruction of
diff --git a/content/browser/renderer_host/navigation_request.cc b/content/browser/renderer_host/navigation_request.cc
index 8ae18e3..b1c45d1 100644
--- a/content/browser/renderer_host/navigation_request.cc
+++ b/content/browser/renderer_host/navigation_request.cc
@@ -1808,14 +1808,9 @@
     }
   }
 
-  bool need_url_mapping = NeedFencedFrameURLMapping();
-  frame_tree_node_->SetFencedFrameModeIfNeeded(
-      need_url_mapping ? FrameTreeNode::FencedFrameMode::kOpaque
-                       : FrameTreeNode::FencedFrameMode::kDefault);
-
   // If this is a fenced frame with a urn:uuid, then convert it to a url before
   // starting the navigation; otherwise, proceed directly with the navigation.
-  if (need_url_mapping) {
+  if (NeedFencedFrameURLMapping()) {
     FencedFrameURLMapping& fenced_frame_urls_map = GetFencedFrameURLMap();
 
     // If the mapping finishes synchronously, OnFencedFrameURLMappingComplete
@@ -5058,8 +5053,8 @@
 
   // [frame-src] or [fenced-frame-src]
   if (parent_policies) {
-    bool is_opaque_fenced_frame = frame_tree_node_->fenced_frame_mode() ==
-                                  FrameTreeNode::FencedFrameMode::kOpaque;
+    bool is_opaque_fenced_frame = frame_tree_node_->GetFencedFrameMode() ==
+                                  blink::mojom::FencedFrameMode::kOpaqueAds;
     if (!IsAllowedByCSPDirective(
             parent_policies->content_security_policies, &parent_context,
             frame_tree_node_->IsFencedFrameRoot()
diff --git a/content/browser/renderer_host/render_frame_host_impl.cc b/content/browser/renderer_host/render_frame_host_impl.cc
index 9e820792..be78955 100644
--- a/content/browser/renderer_host/render_frame_host_impl.cc
+++ b/content/browser/renderer_host/render_frame_host_impl.cc
@@ -7203,7 +7203,18 @@
 void RenderFrameHostImpl::CreateFencedFrame(
     mojo::PendingAssociatedReceiver<blink::mojom::FencedFrameOwnerHost>
         pending_receiver,
+    blink::mojom::FencedFrameMode mode,
     CreateFencedFrameCallback callback) {
+  // We should defer fenced frame creation during prerendering, so creation at
+  // this point is an error.
+  if (GetLifecycleState() == RenderFrameHost::LifecycleState::kPrerendering) {
+    bad_message::ReceivedBadMessage(GetProcess(),
+                                    bad_message::FF_CREATE_WHILE_PRERENDERING);
+    std::move(callback).Run(0, blink::mojom::FrameReplicationState::New(),
+                            blink::RemoteFrameToken(),
+                            base::UnguessableToken::Create());
+    return;
+  }
   if (!blink::features::IsFencedFramesEnabled() ||
       !blink::features::IsFencedFramesMPArchBased()) {
     bad_message::ReceivedBadMessage(
@@ -7225,7 +7236,7 @@
     return;
   }
   fenced_frames_.push_back(
-      std::make_unique<FencedFrame>(weak_ptr_factory_.GetSafeRef()));
+      std::make_unique<FencedFrame>(weak_ptr_factory_.GetSafeRef(), mode));
   FencedFrame* fenced_frame = fenced_frames_.back().get();
   fenced_frame->Bind(std::move(pending_receiver));
 
diff --git a/content/browser/renderer_host/render_frame_host_impl.h b/content/browser/renderer_host/render_frame_host_impl.h
index d7eae7b..e357de40 100644
--- a/content/browser/renderer_host/render_frame_host_impl.h
+++ b/content/browser/renderer_host/render_frame_host_impl.h
@@ -2677,6 +2677,7 @@
   void CreateFencedFrame(
       mojo::PendingAssociatedReceiver<blink::mojom::FencedFrameOwnerHost>
           pending_receiver,
+      blink::mojom::FencedFrameMode mode,
       CreateFencedFrameCallback callback) override;
   void GetKeepAliveHandleFactory(
       mojo::PendingReceiver<blink::mojom::KeepAliveHandleFactory> receiver)
diff --git a/content/browser/security_exploit_browsertest.cc b/content/browser/security_exploit_browsertest.cc
index b693bbb..03f59cf 100644
--- a/content/browser/security_exploit_browsertest.cc
+++ b/content/browser/security_exploit_browsertest.cc
@@ -91,6 +91,7 @@
 #include "third_party/blink/public/common/navigation/navigation_policy.h"
 #include "third_party/blink/public/mojom/blob/blob_url_store.mojom-test-utils.h"
 #include "third_party/blink/public/mojom/choosers/file_chooser.mojom.h"
+#include "third_party/blink/public/mojom/fenced_frame/fenced_frame.mojom.h"
 #include "third_party/blink/public/mojom/frame/frame.mojom-test-utils.h"
 #include "third_party/blink/public/mojom/frame/frame.mojom.h"
 #include "third_party/blink/public/mojom/loader/mixed_content.mojom.h"
@@ -1896,7 +1897,7 @@
       compromised_rfh->GetProcess());
   static_cast<mojom::FrameHost*>(compromised_rfh)
       ->CreateFencedFrame(
-          std::move(receiver),
+          std::move(receiver), blink::mojom::FencedFrameMode::kDefault,
           base::BindOnce([](int, blink::mojom::FrameReplicationStatePtr,
                             const blink::RemoteFrameToken&,
                             const base::UnguessableToken&) {}));
diff --git a/content/browser/site_instance_impl.cc b/content/browser/site_instance_impl.cc
index a2911a1b..a076b90 100644
--- a/content/browser/site_instance_impl.cc
+++ b/content/browser/site_instance_impl.cc
@@ -745,6 +745,17 @@
   return site_info_.RequiresDedicatedProcess(GetIsolationContext());
 }
 
+bool SiteInstanceImpl::RequiresOriginKeyedProcess() {
+  DCHECK_CURRENTLY_ON(BrowserThread::UI);
+  if (!has_site_)
+    return false;
+
+  // TODO(wjmaclean): once SiteInstanceGroups are ready we may give logically
+  // (same-process) isolated origins their own SiteInstances ... in that case we
+  // should consider updating this function.
+  return site_info_.requires_origin_keyed_process();
+}
+
 void SiteInstanceImpl::IncrementRelatedActiveContentsCount() {
   browsing_instance_->increment_active_contents_count();
 }
diff --git a/content/browser/site_instance_impl.h b/content/browser/site_instance_impl.h
index 5349a68..74136c8 100644
--- a/content/browser/site_instance_impl.h
+++ b/content/browser/site_instance_impl.h
@@ -125,6 +125,7 @@
   bool IsRelatedSiteInstance(const SiteInstance* instance) override;
   size_t GetRelatedActiveContentsCount() override;
   bool RequiresDedicatedProcess() override;
+  bool RequiresOriginKeyedProcess() override;
   bool IsSameSiteWithURL(const GURL& url) override;
   bool IsGuest() override;
   SiteInstanceProcessAssignment GetLastProcessAssignmentOutcome() override;
diff --git a/content/browser/webid/federated_auth_request_impl_unittest.cc b/content/browser/webid/federated_auth_request_impl_unittest.cc
index b126e3f7..869b14e 100644
--- a/content/browser/webid/federated_auth_request_impl_unittest.cc
+++ b/content/browser/webid/federated_auth_request_impl_unittest.cc
@@ -143,14 +143,6 @@
   bool customized_dialog;
 } MockConfiguration;
 
-// absl::optional fields should be nullopt to prevent the corresponding
-// methods from having EXPECT_CALL set on the mocks.
-typedef struct {
-  RequestParameters inputs;
-  RequestExpectations expected;
-  MockConfiguration config;
-} AuthRequestTestCase;
-
 static const MockClientIdConfiguration kDefaultClientMetadata{
     FetchStatus::kSuccess, kPrivacyPolicyUrl, kTermsOfServiceUrl};
 
@@ -347,10 +339,8 @@
   void RunAuthTest(const RequestParameters& request_parameters,
                    const RequestExpectations& expectation,
                    const MockConfiguration& configuration) {
-    AuthRequestTestCase test_case = {request_parameters, expectation,
-                                     configuration};
     SetupIdpNetworkRequestManager(GURL(request_parameters.provider));
-    SetMockExpectations(test_case);
+    SetMockExpectations(request_parameters, expectation, configuration);
     auto auth_response = PerformAuthRequest(
         GURL(request_parameters.provider), request_parameters.client_id,
         request_parameters.nonce, request_parameters.prefer_auto_sign_in);
@@ -465,29 +455,66 @@
     return revoke_helper.status();
   }
 
-  bool IsExpectedFetched(MockConfiguration conf,
+  bool IsExpectedFetched(MockConfiguration config,
                          FetchedEndpoint expected_endpoint) {
-    return (conf.expected_fetched_endpoints & expected_endpoint) != 0;
+    return (config.expected_fetched_endpoints & expected_endpoint) != 0;
   }
 
-  void SetMediatedMockExpectations(const MockConfiguration& conf,
-                                   std::string token,
-                                   bool prefer_auto_sign_in) {
-    if (IsExpectedFetched(conf, FetchedEndpoint::ACCOUNTS)) {
+  void SetMockExpectations(const RequestParameters& request_parameters,
+                           const RequestExpectations& expectation,
+                           const MockConfiguration& config) {
+    if (IsExpectedFetched(config, FetchedEndpoint::MANIFEST)) {
+      EXPECT_CALL(*mock_request_manager_, FetchManifest(_, _, _))
+          .WillOnce(Invoke(
+              [&](absl::optional<int>, absl::optional<int>,
+                  IdpNetworkRequestManager::FetchManifestCallback callback) {
+                IdpNetworkRequestManager::Endpoints endpoints;
+                endpoints.accounts = config.manifest.accounts_endpoint;
+                endpoints.token = config.manifest.token_endpoint;
+                endpoints.client_metadata =
+                    config.manifest.client_metadata_endpoint;
+                std::move(callback).Run(config.manifest.fetch_status, endpoints,
+                                        IdentityProviderMetadata());
+              }));
+    } else {
+      EXPECT_CALL(*mock_request_manager_, FetchManifest(_, _, _)).Times(0);
+    }
+
+    if (IsExpectedFetched(config, FetchedEndpoint::CLIENT_METADATA)) {
+      EXPECT_CALL(*mock_request_manager_, FetchClientMetadata(_, _, _))
+          .WillOnce(
+              Invoke([&](const GURL&, const std::string& client_id,
+                         IdpNetworkRequestManager::FetchClientMetadataCallback
+                             callback) {
+                EXPECT_EQ(request_parameters.client_id, client_id);
+                std::move(callback).Run(
+                    config.client_metadata.fetch_status,
+                    IdpNetworkRequestManager::ClientMetadata{
+                        config.client_metadata.privacy_policy_url,
+                        config.client_metadata.terms_of_service_url});
+              }));
+    } else {
+      EXPECT_CALL(*mock_request_manager_, FetchClientMetadata(_, _, _))
+          .Times(0);
+    }
+
+    if (IsExpectedFetched(config, FetchedEndpoint::ACCOUNTS)) {
       EXPECT_CALL(*mock_request_manager_, SendAccountsRequest(_, _, _))
           .WillOnce(Invoke(
               [&](const GURL&, const std::string&,
                   IdpNetworkRequestManager::AccountsRequestCallback callback) {
-                std::move(callback).Run(conf.accounts_response, conf.accounts);
+                std::move(callback).Run(config.accounts_response,
+                                        config.accounts);
               }));
     } else {
       EXPECT_CALL(*mock_request_manager_, SendAccountsRequest(_, _, _))
           .Times(0);
     }
 
-    if (IsExpectedFetched(conf, FetchedEndpoint::ACCOUNTS) &&
-        conf.accounts_response == FetchStatus::kSuccess) {
-      if (!prefer_auto_sign_in && !conf.customized_dialog) {
+    if (IsExpectedFetched(config, FetchedEndpoint::ACCOUNTS) &&
+        config.accounts_response == FetchStatus::kSuccess) {
+      if (!request_parameters.prefer_auto_sign_in &&
+          !config.customized_dialog) {
         // Expects a dialog if prefer_auto_sign_in is not set by RP. However,
         // even though the bit is set we may not exercise the AutoSignIn flow.
         // e.g. for sign up flow, multiple accounts, user opt-out etc. In this
@@ -515,15 +542,16 @@
           .Times(0);
     }
 
-    if (IsExpectedFetched(conf, FetchedEndpoint::TOKEN)) {
-      auto delivered_token =
-          conf.token_response == FetchStatus::kSuccess ? token : std::string();
+    if (IsExpectedFetched(config, FetchedEndpoint::TOKEN)) {
+      auto delivered_token = config.token_response == FetchStatus::kSuccess
+                                 ? config.token
+                                 : std::string();
       EXPECT_CALL(*mock_request_manager_, SendTokenRequest(_, _, _, _))
           .WillOnce(Invoke(
               [=](const GURL& idp_signin_url, const std::string& account_id,
                   const std::string& request,
                   IdpNetworkRequestManager::TokenRequestCallback callback) {
-                std::move(callback).Run(conf.token_response, delivered_token);
+                std::move(callback).Run(config.token_response, delivered_token);
               }));
       task_environment()->FastForwardBy(base::Seconds(3));
     } else {
@@ -532,47 +560,6 @@
     }
   }
 
-  void SetMockExpectations(const AuthRequestTestCase& test_case) {
-    if (IsExpectedFetched(test_case.config, FetchedEndpoint::MANIFEST)) {
-      EXPECT_CALL(*mock_request_manager_, FetchManifest(_, _, _))
-          .WillOnce(Invoke(
-              [&](absl::optional<int>, absl::optional<int>,
-                  IdpNetworkRequestManager::FetchManifestCallback callback) {
-                IdpNetworkRequestManager::Endpoints endpoints;
-                endpoints.accounts =
-                    test_case.config.manifest.accounts_endpoint;
-                endpoints.token = test_case.config.manifest.token_endpoint;
-                endpoints.client_metadata =
-                    test_case.config.manifest.client_metadata_endpoint;
-                std::move(callback).Run(test_case.config.manifest.fetch_status,
-                                        endpoints, IdentityProviderMetadata());
-              }));
-    } else {
-      EXPECT_CALL(*mock_request_manager_, FetchManifest(_, _, _)).Times(0);
-    }
-
-    if (IsExpectedFetched(test_case.config, FetchedEndpoint::CLIENT_METADATA)) {
-      EXPECT_CALL(*mock_request_manager_, FetchClientMetadata(_, _, _))
-          .WillOnce(
-              Invoke([&](const GURL&, const std::string& client_id,
-                         IdpNetworkRequestManager::FetchClientMetadataCallback
-                             callback) {
-                EXPECT_EQ(test_case.inputs.client_id, client_id);
-                std::move(callback).Run(
-                    test_case.config.client_metadata.fetch_status,
-                    IdpNetworkRequestManager::ClientMetadata{
-                        test_case.config.client_metadata.privacy_policy_url,
-                        test_case.config.client_metadata.terms_of_service_url});
-              }));
-    } else {
-      EXPECT_CALL(*mock_request_manager_, FetchClientMetadata(_, _, _))
-          .Times(0);
-    }
-
-    SetMediatedMockExpectations(test_case.config, test_case.config.token,
-                                test_case.inputs.prefer_auto_sign_in);
-  }
-
   // Expectations have to be set explicitly in advance using
   // logout_return_count() and logout_requests().
   void SetLogoutMockExpectations() {
diff --git a/content/common/frame.mojom b/content/common/frame.mojom
index afc0dc3..ec594f2 100644
--- a/content/common/frame.mojom
+++ b/content/common/frame.mojom
@@ -675,7 +675,8 @@
   //                          the "inner" fenced frame FrameTree
   [Sync] CreateFencedFrame(
       pending_associated_receiver<blink.mojom.FencedFrameOwnerHost>
-        fenced_frame)
+        fenced_frame,
+      blink.mojom.FencedFrameMode mode)
       => (int32 proxy_routing_id,
           blink.mojom.FrameReplicationState initial_replication_state,
           blink.mojom.RemoteFrameToken frame_token,
diff --git a/content/public/browser/site_instance.h b/content/public/browser/site_instance.h
index f57e6ac..408a0765 100644
--- a/content/public/browser/site_instance.h
+++ b/content/public/browser/site_instance.h
@@ -169,6 +169,10 @@
   // process. This only returns true under the "site per process" process model.
   virtual bool RequiresDedicatedProcess() = 0;
 
+  // Returns true if this SiteInstance is for a process-isolated origin with its
+  // own OriginAgentCluster.
+  virtual bool RequiresOriginKeyedProcess() = 0;
+
   // Return whether this SiteInstance and the provided |url| are part of the
   // same web site, for the purpose of assigning them to processes accordingly.
   // The decision is currently based on the registered domain of the URLs
diff --git a/content/public/test/test_renderer_host.h b/content/public/test/test_renderer_host.h
index 2e76323b..a02288f 100644
--- a/content/public/test/test_renderer_host.h
+++ b/content/public/test/test_renderer_host.h
@@ -22,6 +22,7 @@
 #include "testing/gtest/include/gtest/gtest.h"
 #include "third_party/blink/public/common/input/synthetic_web_input_event_builders.h"
 #include "third_party/blink/public/common/input/web_input_event.h"
+#include "third_party/blink/public/mojom/fenced_frame/fenced_frame.mojom.h"
 #include "ui/base/page_transition_types.h"
 
 #if defined(USE_AURA)
@@ -137,7 +138,9 @@
   virtual void SimulateManifestURLUpdate(const GURL& manifest_url) = 0;
 
   // Creates and appends a fenced frame.
-  virtual RenderFrameHost* AppendFencedFrame() = 0;
+  virtual RenderFrameHost* AppendFencedFrame(
+      blink::mojom::FencedFrameMode mode =
+          blink::mojom::FencedFrameMode::kDefault) = 0;
 };
 
 // An interface and utility for driving tests of RenderViewHost.
diff --git a/content/renderer/render_frame_impl.cc b/content/renderer/render_frame_impl.cc
index c929696..e0d4faf8 100644
--- a/content/renderer/render_frame_impl.cc
+++ b/content/renderer/render_frame_impl.cc
@@ -3437,6 +3437,27 @@
   return GetRemoteAssociatedInterfaces();
 }
 
+namespace {
+
+// Emit the trace event using a helper as we:
+// a) want to ensure that the trace event covers the entire function.
+// b) we want to emit the new child routing id as an argument.
+// c) child routing id becomes available only after a sync call.
+struct CreateChildFrameTraceEvent {
+  explicit CreateChildFrameTraceEvent(int routing_id) {
+    TRACE_EVENT_BEGIN("navigation,rail", "RenderFrameImpl::createChildFrame",
+                      "routing_id", routing_id);
+  }
+
+  ~CreateChildFrameTraceEvent() {
+    TRACE_EVENT_END("navigation,rail", "child_routing_id", child_routing_id);
+  }
+
+  int child_routing_id = -1;
+};
+
+}  // namespace
+
 blink::WebLocalFrame* RenderFrameImpl::CreateChildFrame(
     blink::mojom::TreeScopeType scope,
     const blink::WebString& name,
@@ -3445,6 +3466,10 @@
     const blink::WebFrameOwnerProperties& frame_owner_properties,
     blink::FrameOwnerElementType frame_owner_element_type,
     blink::WebPolicyContainerBindParams policy_container_bind_params) {
+  // Tracing analysis uses this to find main frames when this value is
+  // MSG_ROUTING_NONE, and build the frame tree otherwise.
+  CreateChildFrameTraceEvent trace_event(routing_id_);
+
   // Allocate child routing ID. This is a synchronous call.
   int child_routing_id;
   blink::LocalFrameToken frame_token;
@@ -3453,6 +3478,7 @@
           child_routing_id, frame_token, devtools_frame_token)) {
     return nullptr;
   }
+  trace_event.child_routing_id = child_routing_id;
 
   // The unique name generation logic was moved out of Blink, so for historical
   // reasons, unique name generation needs to take something called the
@@ -3491,11 +3517,6 @@
       blink::mojom::FrameOwnerProperties::From(frame_owner_properties),
       frame_owner_element_type);
 
-  // Tracing analysis uses this to find main frames when this value is
-  // MSG_ROUTING_NONE, and build the frame tree otherwise.
-  TRACE_EVENT2("navigation,rail", "RenderFrameImpl::createChildFrame", "id",
-               routing_id_, "child", child_routing_id);
-
   // Create the RenderFrame and WebLocalFrame, linking the two.
   RenderFrameImpl* child_render_frame = RenderFrameImpl::Create(
       agent_scheduling_group_, render_view_, child_routing_id,
@@ -3569,16 +3590,16 @@
     const blink::WebElement& fenced_frame,
     blink::CrossVariantMojoAssociatedReceiver<
         blink::mojom::FencedFrameOwnerHostInterfaceBase> receiver,
-    blink::mojom::FencedFrameMode) {
+    blink::mojom::FencedFrameMode mode) {
   int proxy_routing_id = MSG_ROUTING_NONE;
   blink::mojom::FrameReplicationStatePtr initial_replicated_state =
       blink::mojom::FrameReplicationState::New();
   blink::RemoteFrameToken frame_token;
   base::UnguessableToken devtools_frame_token;
 
-  GetFrameHost()->CreateFencedFrame(std::move(receiver), &proxy_routing_id,
-                                    &initial_replicated_state, &frame_token,
-                                    &devtools_frame_token);
+  GetFrameHost()->CreateFencedFrame(
+      std::move(receiver), mode, &proxy_routing_id, &initial_replicated_state,
+      &frame_token, &devtools_frame_token);
 
   RenderFrameProxy* proxy = RenderFrameProxy::CreateProxyForPortalOrFencedFrame(
       agent_scheduling_group_, this, proxy_routing_id, frame_token,
diff --git a/content/test/gpu/gpu_tests/test_expectations/webgl_conformance_expectations.txt b/content/test/gpu/gpu_tests/test_expectations/webgl_conformance_expectations.txt
index a156cdc..01a67b0 100644
--- a/content/test/gpu/gpu_tests/test_expectations/webgl_conformance_expectations.txt
+++ b/content/test/gpu/gpu_tests/test_expectations/webgl_conformance_expectations.txt
@@ -715,6 +715,7 @@
 
 # All platforms, Vulkan backend
 crbug.com/1309304 [ angle-swiftshader passthrough ] conformance/rendering/out-of-bounds-array-buffers.html [ Failure ]
+crbug.com/1309304 [ angle-swiftshader passthrough ] conformance/rendering/out-of-bounds-index-buffers.html [ Failure ]
 
 #######################################################################
 # Automated Entries After This Point - Do Not Manually Add Below Here #
diff --git a/content/test/test_render_frame.cc b/content/test/test_render_frame.cc
index 68d4358..3a5b41d4 100644
--- a/content/test/test_render_frame.cc
+++ b/content/test/test_render_frame.cc
@@ -167,6 +167,7 @@
 
   void CreateFencedFrame(
       mojo::PendingAssociatedReceiver<blink::mojom::FencedFrameOwnerHost>,
+      blink::mojom::FencedFrameMode,
       CreateFencedFrameCallback) override {
     NOTREACHED() << "At the moment, content::FencedFrame is not used in any "
                     "unit tests, so this path should not be hit";
diff --git a/content/test/test_render_frame_host.cc b/content/test/test_render_frame_host.cc
index 1ae1918..a7902b6 100644
--- a/content/test/test_render_frame_host.cc
+++ b/content/test/test_render_frame_host.cc
@@ -253,9 +253,10 @@
   GetPage().UpdateManifestUrl(manifest_url);
 }
 
-TestRenderFrameHost* TestRenderFrameHost::AppendFencedFrame() {
+TestRenderFrameHost* TestRenderFrameHost::AppendFencedFrame(
+    blink::mojom::FencedFrameMode mode) {
   fenced_frames_.push_back(
-      std::make_unique<FencedFrame>(weak_ptr_factory_.GetSafeRef()));
+      std::make_unique<FencedFrame>(weak_ptr_factory_.GetSafeRef(), mode));
   return static_cast<TestRenderFrameHost*>(
       fenced_frames_.back().get()->GetInnerRoot());
 }
diff --git a/content/test/test_render_frame_host.h b/content/test/test_render_frame_host.h
index dccdced..710fa2f4 100644
--- a/content/test/test_render_frame_host.h
+++ b/content/test/test_render_frame_host.h
@@ -103,7 +103,9 @@
   const std::vector<std::string>& GetConsoleMessages() override;
   int GetHeavyAdIssueCount(HeavyAdIssueType type) override;
   void SimulateManifestURLUpdate(const GURL& manifest_url) override;
-  TestRenderFrameHost* AppendFencedFrame() override;
+  TestRenderFrameHost* AppendFencedFrame(
+      blink::mojom::FencedFrameMode mode =
+          blink::mojom::FencedFrameMode::kDefault) override;
 
   void SendNavigate(int nav_entry_id,
                     bool did_create_new_entry,
diff --git a/docs/python3_migration.md b/docs/python3_migration.md
index 3c197539..3fbdcad 100644
--- a/docs/python3_migration.md
+++ b/docs/python3_migration.md
@@ -114,9 +114,7 @@
 
 Test targets that run by invoking python scripts (like telemetry_unittests
 or blink_web_tests) should eventually migrate to using the [script_test]
-GN templates. Once you do that, they will use Python3 by default. However,
-some tests may specify `run_under_python2 = true` as a template variable
-to use Python2, so when you're ready to test Python3, just delete that line.
+GN templates. Once you do that, they will use Python3 by default.
 
 Some tests still need to be migrated to `script_test()`
 ([crbug.com/1208648](https://crbug.com/1208648)). The process for
diff --git a/extensions/browser/api/audio/audio_api.cc b/extensions/browser/api/audio/audio_api.cc
index 7bddfdb..9271f11 100644
--- a/extensions/browser/api/audio/audio_api.cc
+++ b/extensions/browser/api/audio/audio_api.cc
@@ -48,11 +48,13 @@
       .is_available();
 }
 
-bool CanReceiveDeprecatedAudioEvent(content::BrowserContext* browser_context,
-                                    Feature::Context target_context,
-                                    const Extension* extension,
-                                    Event* event,
-                                    const base::DictionaryValue* filter) {
+bool CanReceiveDeprecatedAudioEvent(
+    content::BrowserContext* browser_context,
+    Feature::Context target_context,
+    const Extension* extension,
+    const base::DictionaryValue* filter,
+    std::unique_ptr<base::Value::List>* event_args,
+    mojom::EventFilteringInfoPtr* event_filtering_info_out) {
   return CanUseDeprecatedAudioApi(extension);
 }
 
diff --git a/extensions/browser/api/hid/hid_device_manager.cc b/extensions/browser/api/hid/hid_device_manager.cc
index e453371..246ae16 100644
--- a/extensions/browser/api/hid/hid_device_manager.cc
+++ b/extensions/browser/api/hid/hid_device_manager.cc
@@ -73,13 +73,15 @@
   }
 }
 
-bool WillDispatchDeviceEvent(base::WeakPtr<HidDeviceManager> device_manager,
-                             const device::mojom::HidDeviceInfo& device_info,
-                             content::BrowserContext* browser_context,
-                             Feature::Context target_context,
-                             const Extension* extension,
-                             Event* event,
-                             const base::DictionaryValue* listener_filter) {
+bool WillDispatchDeviceEvent(
+    base::WeakPtr<HidDeviceManager> device_manager,
+    const device::mojom::HidDeviceInfo& device_info,
+    content::BrowserContext* browser_context,
+    Feature::Context target_context,
+    const Extension* extension,
+    const base::DictionaryValue* listener_filter,
+    std::unique_ptr<base::Value::List>* event_args_out,
+    mojom::EventFilteringInfoPtr* event_filtering_info_out) {
   if (device_manager && extension) {
     return device_manager->HasPermission(extension, device_info, false);
   }
diff --git a/extensions/browser/api/printer_provider/printer_provider_api.cc b/extensions/browser/api/printer_provider/printer_provider_api.cc
index f84ee8d..d2ba1793 100644
--- a/extensions/browser/api/printer_provider/printer_provider_api.cc
+++ b/extensions/browser/api/printer_provider/printer_provider_api.cc
@@ -297,12 +297,14 @@
   // in the event. If the extension listens to the event, it's added to the set
   // of |request| sources. |request| is |GetPrintersRequest| object associated
   // with the event.
-  bool WillRequestPrinters(int request_id,
-                           content::BrowserContext* browser_context,
-                           Feature::Context target_context,
-                           const Extension* extension,
-                           Event* event,
-                           const base::DictionaryValue* listener_filter);
+  bool WillRequestPrinters(
+      int request_id,
+      content::BrowserContext* browser_context,
+      Feature::Context target_context,
+      const Extension* extension,
+      const base::DictionaryValue* listener_filter,
+      std::unique_ptr<base::Value::List>* event_args_out,
+      mojom::EventFilteringInfoPtr* event_filtering_info_out);
 
   raw_ptr<content::BrowserContext> browser_context_;
 
@@ -759,8 +761,9 @@
     content::BrowserContext* browser_context,
     Feature::Context target_context,
     const Extension* extension,
-    Event* event,
-    const base::DictionaryValue* listener_filter) {
+    const base::DictionaryValue* listener_filter,
+    std::unique_ptr<base::Value::List>* event_args_out,
+    mojom::EventFilteringInfoPtr* event_filtering_info_out) {
   if (!extension)
     return false;
   EventRouter* event_router = EventRouter::Get(browser_context_);
diff --git a/extensions/browser/api/usb/usb_device_manager.cc b/extensions/browser/api/usb/usb_device_manager.cc
index 8a5c742..d698cc9 100644
--- a/extensions/browser/api/usb/usb_device_manager.cc
+++ b/extensions/browser/api/usb/usb_device_manager.cc
@@ -64,12 +64,14 @@
 
 // Returns true if the given extension has permission to receive events
 // regarding this device.
-bool WillDispatchDeviceEvent(const device::mojom::UsbDeviceInfo& device_info,
-                             content::BrowserContext* browser_context,
-                             Feature::Context target_context,
-                             const Extension* extension,
-                             Event* event,
-                             const base::DictionaryValue* listener_filter) {
+bool WillDispatchDeviceEvent(
+    const device::mojom::UsbDeviceInfo& device_info,
+    content::BrowserContext* browser_context,
+    Feature::Context target_context,
+    const Extension* extension,
+    const base::DictionaryValue* listener_filter,
+    std::unique_ptr<base::Value::List>* event_args_out,
+    mojom::EventFilteringInfoPtr* event_filtering_info_out) {
   // Check install-time and optional permissions.
   std::unique_ptr<UsbDevicePermission::CheckParam> param =
       UsbDevicePermission::CheckParam::ForUsbDevice(extension, device_info);
diff --git a/extensions/browser/event_router.cc b/extensions/browser/event_router.cc
index a843c1f..aaa47d0e 100644
--- a/extensions/browser/event_router.cc
+++ b/extensions/browser/event_router.cc
@@ -968,17 +968,33 @@
     return;
   }
 
+  std::unique_ptr<base::Value::List> modified_event_args;
+  mojom::EventFilteringInfoPtr modified_event_filter_info;
   if (!event->will_dispatch_callback.is_null() &&
-      !event->will_dispatch_callback.Run(listener_context, target_context,
-                                         extension, event, listener_filter)) {
+      !event->will_dispatch_callback.Run(
+          listener_context, target_context, extension, listener_filter,
+          &modified_event_args, &modified_event_filter_info)) {
     return;
   }
 
+  base::ListValue* event_args_to_use = event->event_args.get();
+  std::unique_ptr<base::ListValue> list_modified_event_args;
+  if (modified_event_args) {
+    // If `will_dispatch_callback` provided modified args, use it.
+    list_modified_event_args = base::ListValue::From(
+        std::make_unique<base::Value>(std::move(*modified_event_args)));
+    event_args_to_use = list_modified_event_args.get();
+  }
+
+  mojom::EventFilteringInfoPtr filter_info =
+      modified_event_filter_info ? std::move(modified_event_filter_info)
+                                 : event->filter_info.Clone();
+
   int event_id = g_extension_event_id.GetNext();
   DispatchExtensionMessage(process, worker_thread_id, listener_context,
                            extension_id, event_id, event->event_name,
-                           event->event_args.get(), event->user_gesture,
-                           event->filter_info.Clone());
+                           event_args_to_use, event->user_gesture,
+                           std::move(filter_info));
 
   for (TestObserver& observer : test_observers_)
     observer.OnDidDispatchEventToProcess(*event);
diff --git a/extensions/browser/event_router.h b/extensions/browser/event_router.h
index 82adadd..7b90fb9 100644
--- a/extensions/browser/event_router.h
+++ b/extensions/browser/event_router.h
@@ -516,12 +516,13 @@
 struct Event {
   // This callback should return true if the event should be dispatched to the
   // given context and extension, and false otherwise.
-  using WillDispatchCallback =
-      base::RepeatingCallback<bool(content::BrowserContext*,
-                                   Feature::Context,
-                                   const Extension*,
-                                   Event*,
-                                   const base::DictionaryValue*)>;
+  using WillDispatchCallback = base::RepeatingCallback<bool(
+      content::BrowserContext*,
+      Feature::Context,
+      const Extension*,
+      const base::DictionaryValue*,
+      std::unique_ptr<base::Value::List>* event_args_out,
+      mojom::EventFilteringInfoPtr* event_filtering_info_out)>;
 
   // The identifier for the event, for histograms. In most cases this
   // correlates 1:1 with |event_name|, in some cases events will generate
@@ -550,10 +551,11 @@
   mojom::EventFilteringInfoPtr filter_info;
 
   // If specified, this is called before dispatching an event to each
-  // extension. The third argument is a mutable reference to event_args,
-  // allowing the caller to provide different arguments depending on the
-  // extension and profile. This is guaranteed to be called synchronously with
+  // extension. This is guaranteed to be called synchronously with
   // DispatchEvent, so callers don't need to worry about lifetime.
+  // The args |event_args_out|, |event_filtering_info_out| allows caller to
+  // provide modified `Event::event_args`, `Event::filter_info` depending on the
+  // extension and profile.
   //
   // NOTE: the Extension argument to this may be NULL because it's possible for
   // this event to be dispatched to non-extension processes, like WebUI.
diff --git a/extensions/browser/events/lazy_event_dispatcher.cc b/extensions/browser/events/lazy_event_dispatcher.cc
index f30cef7..884be8a2 100644
--- a/extensions/browser/events/lazy_event_dispatcher.cc
+++ b/extensions/browser/events/lazy_event_dispatcher.cc
@@ -85,16 +85,25 @@
   // to avoid lifetime issues. Use a separate copy of the event args, so they
   // last until the event is dispatched.
   if (!dispatched_event->will_dispatch_callback.is_null()) {
+    std::unique_ptr<base::Value::List> modified_event_args;
+    mojom::EventFilteringInfoPtr modified_event_filter_info;
     if (!dispatched_event->will_dispatch_callback.Run(
             dispatch_context.browser_context(),
             // The only lazy listeners belong to an extension's background
             // context (either an event page or a service worker), which are
             // always BLESSED_EXTENSION_CONTEXTs
             extensions::Feature::BLESSED_EXTENSION_CONTEXT, extension,
-            dispatched_event.get(), listener_filter)) {
+            listener_filter, &modified_event_args,
+            &modified_event_filter_info)) {
       // The event has been canceled.
       return true;
     }
+    if (modified_event_args) {
+      dispatched_event->event_args = base::ListValue::From(
+          std::make_unique<base::Value>(std::move(*modified_event_args)));
+    }
+    if (modified_event_filter_info)
+      dispatched_event->filter_info = std::move(modified_event_filter_info);
     // Ensure we don't call it again at dispatch time.
     dispatched_event->will_dispatch_callback.Reset();
   }
diff --git "a/infra/config/generated/builders/ci/GPU FYI Win x64 Builder \050dbg\051/properties.json" "b/infra/config/generated/builders/ci/GPU FYI Win x64 Builder \050dbg\051/properties.json"
index def7636..a6a7b71f 100644
--- "a/infra/config/generated/builders/ci/GPU FYI Win x64 Builder \050dbg\051/properties.json"
+++ "b/infra/config/generated/builders/ci/GPU FYI Win x64 Builder \050dbg\051/properties.json"
@@ -1,9 +1,8 @@
 {
-  "$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": 80,
+    "metrics_project": "chromium-reclient-metrics"
   },
   "$recipe_engine/resultdb/test_presentation": {
     "column_keys": [],
diff --git a/infra/config/generated/builders/ci/GPU FYI Win x64 Builder/properties.json b/infra/config/generated/builders/ci/GPU FYI Win x64 Builder/properties.json
index def7636..a6a7b71f 100644
--- a/infra/config/generated/builders/ci/GPU FYI Win x64 Builder/properties.json
+++ b/infra/config/generated/builders/ci/GPU FYI Win x64 Builder/properties.json
@@ -1,9 +1,8 @@
 {
-  "$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": 80,
+    "metrics_project": "chromium-reclient-metrics"
   },
   "$recipe_engine/resultdb/test_presentation": {
     "column_keys": [],
diff --git "a/infra/config/generated/builders/ci/GPU FYI Win x64 DX12 Vulkan Builder \050dbg\051/properties.json" "b/infra/config/generated/builders/ci/GPU FYI Win x64 DX12 Vulkan Builder \050dbg\051/properties.json"
index def7636..a6a7b71f 100644
--- "a/infra/config/generated/builders/ci/GPU FYI Win x64 DX12 Vulkan Builder \050dbg\051/properties.json"
+++ "b/infra/config/generated/builders/ci/GPU FYI Win x64 DX12 Vulkan Builder \050dbg\051/properties.json"
@@ -1,9 +1,8 @@
 {
-  "$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": 80,
+    "metrics_project": "chromium-reclient-metrics"
   },
   "$recipe_engine/resultdb/test_presentation": {
     "column_keys": [],
diff --git a/infra/config/generated/builders/ci/GPU FYI Win x64 DX12 Vulkan Builder/properties.json b/infra/config/generated/builders/ci/GPU FYI Win x64 DX12 Vulkan Builder/properties.json
index def7636..a6a7b71f 100644
--- a/infra/config/generated/builders/ci/GPU FYI Win x64 DX12 Vulkan Builder/properties.json
+++ b/infra/config/generated/builders/ci/GPU FYI Win x64 DX12 Vulkan Builder/properties.json
@@ -1,9 +1,8 @@
 {
-  "$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": 80,
+    "metrics_project": "chromium-reclient-metrics"
   },
   "$recipe_engine/resultdb/test_presentation": {
     "column_keys": [],
diff --git a/infra/config/generated/builders/ci/GPU FYI XR Win x64 Builder/properties.json b/infra/config/generated/builders/ci/GPU FYI XR Win x64 Builder/properties.json
index def7636..a6a7b71f 100644
--- a/infra/config/generated/builders/ci/GPU FYI XR Win x64 Builder/properties.json
+++ b/infra/config/generated/builders/ci/GPU FYI XR Win x64 Builder/properties.json
@@ -1,9 +1,8 @@
 {
-  "$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": 80,
+    "metrics_project": "chromium-reclient-metrics"
   },
   "$recipe_engine/resultdb/test_presentation": {
     "column_keys": [],
diff --git "a/infra/config/generated/builders/ci/GPU Win x64 Builder \050dbg\051/properties.json" "b/infra/config/generated/builders/ci/GPU Win x64 Builder \050dbg\051/properties.json"
index 6208bc0..47af5e9c 100644
--- "a/infra/config/generated/builders/ci/GPU Win x64 Builder \050dbg\051/properties.json"
+++ "b/infra/config/generated/builders/ci/GPU Win x64 Builder \050dbg\051/properties.json"
@@ -1,9 +1,8 @@
 {
-  "$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": 80,
+    "metrics_project": "chromium-reclient-metrics"
   },
   "$recipe_engine/resultdb/test_presentation": {
     "column_keys": [],
diff --git a/infra/config/generated/builders/ci/ios-asan/properties.json b/infra/config/generated/builders/ci/ios-asan/properties.json
index 8b9835b9..c18caf9e 100644
--- a/infra/config/generated/builders/ci/ios-asan/properties.json
+++ b/infra/config/generated/builders/ci/ios-asan/properties.json
@@ -1,23 +1,4 @@
 {
-  "$build/archive": {
-    "archive_datas": [
-      {
-        "archive_type": "ARCHIVE_TYPE_ZIP",
-        "dirs": [
-          "ios_cwt_chromedriver_tests_module.xctest",
-          "ios_cwt_chromedriver_tests_module-Runner.app",
-          "ios_cwt_chromedriver_tests.app",
-          "ios",
-          "testing"
-        ],
-        "files": [
-          "libclang_rt.asan_iossim_dynamic.dylib"
-        ],
-        "gcs_bucket": "chromium-browser-asan",
-        "gcs_path": "ios-release/asan-ios-release-{%timestamp%}.zip"
-      }
-    ]
-  },
   "$build/goma": {
     "rpc_extra_params": "?prod",
     "server_host": "goma.chromium.org",
diff --git a/infra/config/subprojects/chromium/ci/chromium.fyi.star b/infra/config/subprojects/chromium/ci/chromium.fyi.star
index 60e2c72a..e3109d8 100644
--- a/infra/config/subprojects/chromium/ci/chromium.fyi.star
+++ b/infra/config/subprojects/chromium/ci/chromium.fyi.star
@@ -1329,27 +1329,6 @@
         category = "iOS",
         short_name = "asan",
     ),
-    properties = {
-        "$build/archive": {
-            "archive_datas": [
-                {
-                    "archive_type": "ARCHIVE_TYPE_ZIP",
-                    "files": [
-                        "libclang_rt.asan_iossim_dynamic.dylib",
-                    ],
-                    "dirs": [
-                        "ios_cwt_chromedriver_tests_module.xctest",
-                        "ios_cwt_chromedriver_tests_module-Runner.app",
-                        "ios_cwt_chromedriver_tests.app",
-                        "ios",
-                        "testing",
-                    ],
-                    "gcs_bucket": "chromium-browser-asan",
-                    "gcs_path": "ios-release/asan-ios-release-{%timestamp%}.zip",
-                },
-            ],
-        },
-    },
 )
 
 fyi_ios_builder(
diff --git a/infra/config/subprojects/chromium/ci/chromium.gpu.fyi.star b/infra/config/subprojects/chromium/ci/chromium.gpu.fyi.star
index 929332c2..dd34789 100644
--- a/infra/config/subprojects/chromium/ci/chromium.gpu.fyi.star
+++ b/infra/config/subprojects/chromium/ci/chromium.gpu.fyi.star
@@ -628,6 +628,9 @@
         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(
@@ -636,6 +639,9 @@
         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(
@@ -644,6 +650,9 @@
         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(
@@ -652,6 +661,9 @@
         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(
@@ -660,6 +672,9 @@
         category = "Windows|Builder|XR",
         short_name = "x64",
     ),
+    goma_backend = None,
+    reclient_jobs = rbe_jobs.LOW_JOBS_FOR_CI,
+    reclient_instance = rbe_instance.DEFAULT,
 )
 
 _gpu_fyi_windows_builder_shadow(
diff --git a/infra/config/subprojects/chromium/ci/chromium.gpu.star b/infra/config/subprojects/chromium/ci/chromium.gpu.star
index 9cff9001..c5c6c7de 100644
--- a/infra/config/subprojects/chromium/ci/chromium.gpu.star
+++ b/infra/config/subprojects/chromium/ci/chromium.gpu.star
@@ -119,6 +119,9 @@
     ),
     sheriff_rotations = args.ignore_default(None),
     tree_closing = False,
+    goma_backend = None,
+    reclient_jobs = rbe_jobs.LOW_JOBS_FOR_CI,
+    reclient_instance = rbe_instance.DEFAULT,
 )
 
 ci.thin_tester(
diff --git a/ios/web/public/thread/web_task_traits.h b/ios/web/public/thread/web_task_traits.h
index 375cf01..d757873 100644
--- a/ios/web/public/thread/web_task_traits.h
+++ b/ios/web/public/thread/web_task_traits.h
@@ -26,10 +26,10 @@
 // to a WebThread.
 //
 // To post a task to the UI thread (analogous for IO thread):
-//     base::PostTask(FROM_HERE, {WebThread::UI}, task);
+//     web::GetUIThreadTaskRunner({})->PostTask(FROM_HERE, task);
 //
 // To obtain a TaskRunner for the UI thread (analogous for the IO thread):
-//     base::CreateSingleThreadTaskRunner({WebThread::UI});
+//     web::GetUIThreadTaskRunner({});
 //
 // Tasks posted to the same WebThread with the same traits will be executed
 // in the order they were posted, regardless of the TaskRunners they were
diff --git a/media/mojo/mojom/stable/BUILD.gn b/media/mojo/mojom/stable/BUILD.gn
index 6ea5af91..1d48e02 100644
--- a/media/mojo/mojom/stable/BUILD.gn
+++ b/media/mojo/mojom/stable/BUILD.gn
@@ -150,19 +150,28 @@
 mojom("native_pixmap_handle") {
   sources = [ "native_pixmap_handle.mojom" ]
 
-  public_deps = [ "//ui/gfx/mojom:native_handle_types" ]
-
-  cpp_typemaps = [
-    {
-      types = [
-        {
-          mojom = "media.stable.mojom.NativePixmapHandle"
-          cpp = "::gfx::NativePixmapHandle"
-          move_only = true
-        },
-      ]
-      traits_headers = [ "native_pixmap_handle_mojom_traits.h" ]
-      traits_sources = [ "native_pixmap_handle_mojom_traits.cc" ]
-    },
-  ]
+  if (is_linux || is_chromeos) {
+    cpp_typemaps = [
+      {
+        types = [
+          {
+            mojom = "media.stable.mojom.NativePixmapHandle"
+            cpp = "::gfx::NativePixmapHandle"
+            move_only = true
+          },
+          {
+            mojom = "media.stable.mojom.NativePixmapPlane"
+            cpp = "::gfx::NativePixmapPlane"
+            move_only = true
+          },
+        ]
+        traits_headers = [
+          "native_pixmap_handle_mojom_traits.h",
+          "//ui/gfx/native_pixmap_handle.h",
+        ]
+        traits_sources = [ "native_pixmap_handle_mojom_traits.cc" ]
+        traits_public_deps = [ "//ui/gfx:memory_buffer" ]
+      },
+    ]
+  }
 }
diff --git a/media/mojo/mojom/stable/native_pixmap_handle.mojom b/media/mojo/mojom/stable/native_pixmap_handle.mojom
index 8484e29e..765e6773 100644
--- a/media/mojo/mojom/stable/native_pixmap_handle.mojom
+++ b/media/mojo/mojom/stable/native_pixmap_handle.mojom
@@ -4,13 +4,23 @@
 
 module media.stable.mojom;
 
-import "ui/gfx/mojom/native_handle_types.mojom";
+// Based on |gfx.mojom.NativePixmapPlane|.
+// Next min field ID: 4
+[Stable]
+struct NativePixmapPlane {
+  uint32 stride@0;
+  uint64 offset@1;
+  uint64 size@2;
+
+  // A platform-specific handle to the underlying memory object.
+  handle<platform> buffer_handle@3;
+};
 
 // Based on |gfx.mojom.NativePixmapHandle|.
 // Next min field ID: 2
 [Stable]
 struct NativePixmapHandle {
-  array<gfx.mojom.NativePixmapPlane> planes@0;
+  array<NativePixmapPlane> planes@0;
   uint64 modifier@1;
 };
 
diff --git a/media/mojo/mojom/stable/native_pixmap_handle_mojom_traits.cc b/media/mojo/mojom/stable/native_pixmap_handle_mojom_traits.cc
index 4348c31..8e695ff 100644
--- a/media/mojo/mojom/stable/native_pixmap_handle_mojom_traits.cc
+++ b/media/mojo/mojom/stable/native_pixmap_handle_mojom_traits.cc
@@ -9,7 +9,7 @@
 // This file contains a variety of conservative compile-time assertions that
 // help us detect changes that may break the backward compatibility requirement
 // of the StableVideoDecoder API. Specifically, we have static_asserts() that
-// ensure the type of the media struct member is *exactly* the same as the
+// ensure the type of the gfx struct member is *exactly* the same as the
 // corresponding mojo struct member. If this changes, we must be careful to
 // validate ranges and avoid implicit conversions.
 //
@@ -19,6 +19,77 @@
 namespace mojo {
 
 // static
+uint32_t StructTraits<
+    media::stable::mojom::NativePixmapPlaneDataView,
+    gfx::NativePixmapPlane>::stride(const gfx::NativePixmapPlane& plane) {
+  static_assert(
+      std::is_same<decltype(::gfx::NativePixmapPlane::stride),
+                   decltype(
+                       media::stable::mojom::NativePixmapPlane::stride)>::value,
+      "Unexpected type for gfx::NativePixmapPlane::stride. If you need to "
+      "change this assertion, please contact chromeos-gfx-video@google.com.");
+
+  return plane.stride;
+}
+
+// static
+uint64_t StructTraits<
+    media::stable::mojom::NativePixmapPlaneDataView,
+    gfx::NativePixmapPlane>::offset(const gfx::NativePixmapPlane& plane) {
+  static_assert(
+      std::is_same<decltype(::gfx::NativePixmapPlane::offset),
+                   decltype(
+                       media::stable::mojom::NativePixmapPlane::offset)>::value,
+      "Unexpected type for gfx::NativePixmapPlane::offset. If you need to "
+      "change this assertion, please contact chromeos-gfx-video@google.com.");
+
+  return plane.offset;
+}
+
+// static
+uint64_t StructTraits<
+    media::stable::mojom::NativePixmapPlaneDataView,
+    gfx::NativePixmapPlane>::size(const gfx::NativePixmapPlane& plane) {
+  static_assert(
+      std::is_same<decltype(::gfx::NativePixmapPlane::size),
+                   decltype(
+                       media::stable::mojom::NativePixmapPlane::size)>::value,
+      "Unexpected type for gfx::NativePixmapPlane::size. If you need to change "
+      "this assertion, please contact chromeos-gfx-video@google.com.");
+
+  return plane.size;
+}
+
+// static
+mojo::PlatformHandle StructTraits<
+    media::stable::mojom::NativePixmapPlaneDataView,
+    gfx::NativePixmapPlane>::buffer_handle(gfx::NativePixmapPlane& plane) {
+  static_assert(
+      std::is_same<decltype(::gfx::NativePixmapPlane::fd),
+                   base::ScopedFD>::value,
+      "Unexpected type for gfx::NativePixmapPlane::fd. If you need to change "
+      "this assertion, please contact chromeos-gfx-video@google.com.");
+  CHECK(plane.fd.is_valid());
+  return mojo::PlatformHandle(std::move(plane.fd));
+}
+
+// static
+bool StructTraits<media::stable::mojom::NativePixmapPlaneDataView,
+                  gfx::NativePixmapPlane>::
+    Read(media::stable::mojom::NativePixmapPlaneDataView data,
+         gfx::NativePixmapPlane* out) {
+  out->stride = data.stride();
+  out->offset = data.offset();
+  out->size = data.size();
+
+  mojo::PlatformHandle handle = data.TakeBufferHandle();
+  if (!handle.is_fd() || !handle.is_valid_fd())
+    return false;
+  out->fd = handle.TakeFD();
+  return true;
+}
+
+// static
 std::vector<gfx::NativePixmapPlane>& StructTraits<
     media::stable::mojom::NativePixmapHandleDataView,
     gfx::NativePixmapHandle>::planes(gfx::NativePixmapHandle& pixmap_handle) {
diff --git a/media/mojo/mojom/stable/native_pixmap_handle_mojom_traits.h b/media/mojo/mojom/stable/native_pixmap_handle_mojom_traits.h
index 911356d..5b611da 100644
--- a/media/mojo/mojom/stable/native_pixmap_handle_mojom_traits.h
+++ b/media/mojo/mojom/stable/native_pixmap_handle_mojom_traits.h
@@ -9,11 +9,27 @@
 
 namespace gfx {
 struct NativePixmapHandle;
+struct NativePixmapPlane;
 }  // namespace gfx
 
 namespace mojo {
 
 template <>
+struct StructTraits<media::stable::mojom::NativePixmapPlaneDataView,
+                    gfx::NativePixmapPlane> {
+  static uint32_t stride(const gfx::NativePixmapPlane& plane);
+
+  static uint64_t offset(const gfx::NativePixmapPlane& plane);
+
+  static uint64_t size(const gfx::NativePixmapPlane& plane);
+
+  static mojo::PlatformHandle buffer_handle(gfx::NativePixmapPlane& plane);
+
+  static bool Read(media::stable::mojom::NativePixmapPlaneDataView data,
+                   gfx::NativePixmapPlane* out);
+};
+
+template <>
 struct StructTraits<media::stable::mojom::NativePixmapHandleDataView,
                     gfx::NativePixmapHandle> {
   static std::vector<gfx::NativePixmapPlane>& planes(
diff --git a/media/mojo/mojom/stable/stable_video_decoder_types_mojom_traits.cc b/media/mojo/mojom/stable/stable_video_decoder_types_mojom_traits.cc
index 28ad47c..00ad8b6 100644
--- a/media/mojo/mojom/stable/stable_video_decoder_types_mojom_traits.cc
+++ b/media/mojo/mojom/stable/stable_video_decoder_types_mojom_traits.cc
@@ -40,8 +40,15 @@
   CHECK(input->HasGpuMemoryBuffer());
   gfx::GpuMemoryBufferHandle gpu_memory_buffer_handle =
       input->GetGpuMemoryBuffer()->CloneHandle();
+
+#if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS)
   CHECK_EQ(gpu_memory_buffer_handle.type, gfx::NATIVE_PIXMAP);
   CHECK(!gpu_memory_buffer_handle.native_pixmap_handle.planes.empty());
+#else
+  // We should not be trying to serialize a media::VideoFrame for the purposes
+  // of this interface outside of Linux and Chrome OS.
+  CHECK(false);
+#endif  // BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS)
 
   return media::stable::mojom::VideoFrameData::NewGpuMemoryBufferData(
       media::stable::mojom::GpuMemoryBufferVideoFrameData::New(
@@ -525,6 +532,7 @@
   return input.id;
 }
 
+#if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS)
 // static
 gfx::NativePixmapHandle StructTraits<
     media::stable::mojom::NativeGpuMemoryBufferHandleDataView,
@@ -533,6 +541,7 @@
   CHECK_EQ(input.type, gfx::NATIVE_PIXMAP);
   return std::move(input.native_pixmap_handle);
 }
+#endif  // BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS)
 
 // static
 bool StructTraits<media::stable::mojom::NativeGpuMemoryBufferHandleDataView,
@@ -542,12 +551,17 @@
   if (!data.ReadId(&output->id))
     return false;
 
-  if (!data.ReadPlatformHandle(&output->native_pixmap_handle))
-    return false;
-
   output->type = gfx::NATIVE_PIXMAP;
 
+#if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS)
+  if (!data.ReadPlatformHandle(&output->native_pixmap_handle))
+    return false;
   return true;
+#else
+  // We should not be trying to de-serialize a gfx::GpuMemoryBufferHandle for
+  // the purposes of this interface outside of Linux and Chrome OS.
+  return false;
+#endif  // BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS)
 }
 
 // static
diff --git a/media/mojo/mojom/stable/stable_video_decoder_types_mojom_traits.h b/media/mojo/mojom/stable/stable_video_decoder_types_mojom_traits.h
index f2caf80..a6475841 100644
--- a/media/mojo/mojom/stable/stable_video_decoder_types_mojom_traits.h
+++ b/media/mojo/mojom/stable/stable_video_decoder_types_mojom_traits.h
@@ -580,8 +580,18 @@
   static const gfx::GpuMemoryBufferId& id(
       const gfx::GpuMemoryBufferHandle& input);
 
+#if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS)
   static gfx::NativePixmapHandle platform_handle(
       gfx::GpuMemoryBufferHandle& input);
+#else
+  static media::stable::mojom::NativePixmapHandlePtr platform_handle(
+      gfx::GpuMemoryBufferHandle& input) {
+    // We should not be trying to serialize a gfx::GpuMemoryBufferHandle for the
+    // purposes of this interface outside of Linux and Chrome OS.
+    CHECK(false);
+    return media::stable::mojom::NativePixmapHandle::New();
+  }
+#endif  // BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS)
 
   static bool Read(
       media::stable::mojom::NativeGpuMemoryBufferHandleDataView data,
diff --git a/media/renderers/paint_canvas_video_renderer.cc b/media/renderers/paint_canvas_video_renderer.cc
index bdaaf118..06eff2d3 100644
--- a/media/renderers/paint_canvas_video_renderer.cc
+++ b/media/renderers/paint_canvas_video_renderer.cc
@@ -568,9 +568,7 @@
       convert_yuva(matrix, libyuv::I444AlphaToARGBMatrix);
       break;
     case PIXEL_FORMAT_YUV420AP10:
-      // TODO(wtc): Use libyuv::I010AlphaToARGBMatrixFilter after
-      // https://crbug.com/libyuv/922 is fixed.
-      convert_yuva16(matrix, libyuv::I010AlphaToARGBMatrix);
+      convert_yuva16_with_filter(matrix, libyuv::I010AlphaToARGBMatrixFilter);
       break;
     case PIXEL_FORMAT_YUV422AP10:
       convert_yuva16_with_filter(matrix, libyuv::I210AlphaToARGBMatrixFilter);
diff --git a/media/renderers/win/media_foundation_renderer.cc b/media/renderers/win/media_foundation_renderer.cc
index 61860c60..442f4d4a 100644
--- a/media/renderers/win/media_foundation_renderer.cc
+++ b/media/renderers/win/media_foundation_renderer.cc
@@ -679,7 +679,7 @@
     return;
   }
 
-  const int kSignificantPlaybackFrames = 1800;  // About 30 fps for 1 minute.
+  const int kSignificantPlaybackFrames = 5400;  // About 30 fps for 3 minutes.
   if (!has_reported_significant_playback_ && cdm_proxy_ &&
       new_stats.video_frames_decoded >= kSignificantPlaybackFrames) {
     has_reported_significant_playback_ = true;
diff --git a/printing/printing_context_system_dialog_win.cc b/printing/printing_context_system_dialog_win.cc
index ba604d3..b7ba6ba 100644
--- a/printing/printing_context_system_dialog_win.cc
+++ b/printing/printing_context_system_dialog_win.cc
@@ -10,12 +10,34 @@
 #include "base/strings/utf_string_conversions.h"
 #include "base/task/current_thread.h"
 #include "printing/backend/win_helper.h"
+#include "printing/buildflags/buildflags.h"
 #include "printing/mojom/print.mojom.h"
 #include "printing/print_settings_initializer_win.h"
 #include "skia/ext/skia_utils_win.h"
 
+#if BUILDFLAG(ENABLE_OOP_PRINTING)
+#include "printing/printing_features.h"
+#endif
+
 namespace printing {
 
+HWND PrintingContextSystemDialogWin::GetWindow() {
+#if BUILDFLAG(ENABLE_OOP_PRINTING)
+  if (features::kEnableOopPrintDriversJobPrint.Get()) {
+    // Delving through the view tree to get to root window happens separately
+    // in the browser process (i.e., not in `PrintingContextSystemDialogWin`)
+    // before sending the identified window owner to the Print Backend service.
+    // This means that this call is happening in the service, and thus should
+    // just use the parent view as-is instead of looking for the root window.
+    // TODO(crbug.com/809738)  Pursue having a service-level instantiation of
+    // `PrintingContextSystemDialogWin` for this behavior.  That would ensure
+    // this logic would be compile-time driven and only invoked by the service.
+    return reinterpret_cast<HWND>(delegate_->GetParentView());
+  }
+#endif
+  return GetRootWindow(delegate_->GetParentView());
+}
+
 PrintingContextSystemDialogWin::PrintingContextSystemDialogWin(
     Delegate* delegate)
     : PrintingContextWin(delegate) {}
@@ -29,7 +51,7 @@
     PrintSettingsCallback callback) {
   DCHECK(!in_print_job_);
 
-  HWND window = GetRootWindow(delegate_->GetParentView());
+  HWND window = GetWindow();
   DCHECK(window);
 
   // Show the OS-dependent dialog box.
diff --git a/printing/printing_context_system_dialog_win.h b/printing/printing_context_system_dialog_win.h
index d44698f..d159465 100644
--- a/printing/printing_context_system_dialog_win.h
+++ b/printing/printing_context_system_dialog_win.h
@@ -36,6 +36,8 @@
  private:
   friend class MockPrintingContextWin;
 
+  HWND GetWindow();
+
   virtual HRESULT ShowPrintDialog(PRINTDLGEX* options);
 
   // Reads the settings from the selected device context. Updates settings_ and
diff --git a/printing/test_printing_context.cc b/printing/test_printing_context.cc
index 38ecb4f..3c996a5 100644
--- a/printing/test_printing_context.cc
+++ b/printing/test_printing_context.cc
@@ -59,7 +59,15 @@
                                              bool is_scripted,
                                              PrintSettingsCallback callback) {
   // Do not actually ask the user with a dialog, just pretend like user
-  // selected the default printer and used the default settings for it.
+  // made some kind of interaction.
+  if (ask_user_for_settings_cancel_) {
+    // Pretend the user hit the Cancel button.
+    std::move(callback).Run(mojom::ResultCode::kCanceled);
+    return;
+  }
+
+  // Pretend the user selected the default printer and used the default
+  // settings for it.
   scoped_refptr<PrintBackend> print_backend =
       PrintBackend::CreateInstance(/*locale=*/std::string());
   std::string printer_name;
@@ -80,6 +88,9 @@
 mojom::ResultCode TestPrintingContext::UseDefaultSettings() {
   scoped_refptr<PrintBackend> print_backend =
       PrintBackend::CreateInstance(/*locale=*/std::string());
+  if (use_default_settings_fails_)
+    return mojom::ResultCode::kFailed;
+
   std::string printer_name;
   mojom::ResultCode result = print_backend->GetDefaultPrinterName(printer_name);
   if (result != mojom::ResultCode::kSuccess)
diff --git a/printing/test_printing_context.h b/printing/test_printing_context.h
index 71b93bd..4841b7c 100644
--- a/printing/test_printing_context.h
+++ b/printing/test_printing_context.h
@@ -56,6 +56,12 @@
     document_done_blocked_by_permissions_ = true;
   }
 
+  // Enables tests to fail with a failed error.
+  void SetUseDefaultSettingsFails() { use_default_settings_fails_ = true; }
+
+  // Enables tests to fail with a canceled error.
+  void SetAskUserForSettingsCanceled() { ask_user_for_settings_cancel_ = true; }
+
   // PrintingContext overrides:
   void AskUserForSettings(int max_pages,
                           bool has_selection,
@@ -84,6 +90,8 @@
 
  private:
   base::flat_map<std::string, std::unique_ptr<PrintSettings>> device_settings_;
+  bool use_default_settings_fails_ = false;
+  bool ask_user_for_settings_cancel_ = false;
   bool new_document_blocked_by_permissions_ = false;
 #if BUILDFLAG(IS_WIN)
   bool render_page_blocked_by_permissions_ = false;
diff --git a/sandbox/win/src/handle_closer_agent.cc b/sandbox/win/src/handle_closer_agent.cc
index a0bb656..1aea0ebb 100644
--- a/sandbox/win/src/handle_closer_agent.cc
+++ b/sandbox/win/src/handle_closer_agent.cc
@@ -7,7 +7,9 @@
 #include <stddef.h>
 
 #include "base/check.h"
+#include "base/logging.h"
 #include "base/win/static_constants.h"
+#include "base/win/win_util.h"
 #include "base/win/windows_version.h"
 #include "sandbox/win/src/win_utils.h"
 #include "third_party/abseil-cpp/absl/types/optional.h"
@@ -144,7 +146,7 @@
 bool HandleCloserAgent::CloseHandles() {
   // Skip closing these handles when Application Verifier is in use in order to
   // avoid invalid-handle exceptions.
-  if (GetModuleHandleA(base::win::kApplicationVerifierDllName))
+  if (base::win::IsAppVerifierLoaded())
     return true;
   // If the accurate handle enumeration fails then fallback to the old brute
   // force approach. This should only happen on Windows 7.
diff --git a/testing/buildbot/chromium.android.fyi.json b/testing/buildbot/chromium.android.fyi.json
index 5bf6966..d5b3f10 100644
--- a/testing/buildbot/chromium.android.fyi.json
+++ b/testing/buildbot/chromium.android.fyi.json
@@ -9581,7 +9581,7 @@
             {
               "cipd_package": "chromium/testing/weblayer-x86",
               "location": "weblayer_instrumentation_test_M100",
-              "revision": "version:100.0.4896.58"
+              "revision": "version:100.0.4896.60"
             },
             {
               "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
@@ -9665,7 +9665,7 @@
             {
               "cipd_package": "chromium/testing/weblayer-x86",
               "location": "weblayer_instrumentation_test_M101",
-              "revision": "version:101.0.4951.9"
+              "revision": "version:101.0.4951.11"
             },
             {
               "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
@@ -10085,7 +10085,7 @@
             {
               "cipd_package": "chromium/testing/weblayer-x86",
               "location": "weblayer_instrumentation_test_M100",
-              "revision": "version:100.0.4896.58"
+              "revision": "version:100.0.4896.60"
             },
             {
               "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
@@ -10169,7 +10169,7 @@
             {
               "cipd_package": "chromium/testing/weblayer-x86",
               "location": "weblayer_instrumentation_test_M101",
-              "revision": "version:101.0.4951.9"
+              "revision": "version:101.0.4951.11"
             },
             {
               "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
diff --git a/testing/buildbot/chromium.android.json b/testing/buildbot/chromium.android.json
index ee310f3..55ff66d 100644
--- a/testing/buildbot/chromium.android.json
+++ b/testing/buildbot/chromium.android.json
@@ -44881,7 +44881,7 @@
             {
               "cipd_package": "chromium/testing/weblayer-x86",
               "location": "weblayer_instrumentation_test_M100",
-              "revision": "version:100.0.4896.58"
+              "revision": "version:100.0.4896.60"
             },
             {
               "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
@@ -44965,7 +44965,7 @@
             {
               "cipd_package": "chromium/testing/weblayer-x86",
               "location": "weblayer_instrumentation_test_M101",
-              "revision": "version:101.0.4951.9"
+              "revision": "version:101.0.4951.11"
             },
             {
               "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
@@ -45385,7 +45385,7 @@
             {
               "cipd_package": "chromium/testing/weblayer-x86",
               "location": "weblayer_instrumentation_test_M100",
-              "revision": "version:100.0.4896.58"
+              "revision": "version:100.0.4896.60"
             },
             {
               "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
@@ -45469,7 +45469,7 @@
             {
               "cipd_package": "chromium/testing/weblayer-x86",
               "location": "weblayer_instrumentation_test_M101",
-              "revision": "version:101.0.4951.9"
+              "revision": "version:101.0.4951.11"
             },
             {
               "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
@@ -45893,7 +45893,7 @@
             {
               "cipd_package": "chromium/testing/weblayer-x86",
               "location": "weblayer_instrumentation_test_M100",
-              "revision": "version:100.0.4896.58"
+              "revision": "version:100.0.4896.60"
             },
             {
               "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
@@ -45977,7 +45977,7 @@
             {
               "cipd_package": "chromium/testing/weblayer-x86",
               "location": "weblayer_instrumentation_test_M101",
-              "revision": "version:101.0.4951.9"
+              "revision": "version:101.0.4951.11"
             },
             {
               "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
@@ -46397,7 +46397,7 @@
             {
               "cipd_package": "chromium/testing/weblayer-x86",
               "location": "weblayer_instrumentation_test_M100",
-              "revision": "version:100.0.4896.58"
+              "revision": "version:100.0.4896.60"
             },
             {
               "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
@@ -46481,7 +46481,7 @@
             {
               "cipd_package": "chromium/testing/weblayer-x86",
               "location": "weblayer_instrumentation_test_M101",
-              "revision": "version:101.0.4951.9"
+              "revision": "version:101.0.4951.11"
             },
             {
               "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
@@ -46972,7 +46972,7 @@
             {
               "cipd_package": "chromium/testing/weblayer-x86",
               "location": "weblayer_instrumentation_test_M100",
-              "revision": "version:100.0.4896.58"
+              "revision": "version:100.0.4896.60"
             },
             {
               "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
@@ -47056,7 +47056,7 @@
             {
               "cipd_package": "chromium/testing/weblayer-x86",
               "location": "weblayer_instrumentation_test_M101",
-              "revision": "version:101.0.4951.9"
+              "revision": "version:101.0.4951.11"
             },
             {
               "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
@@ -47476,7 +47476,7 @@
             {
               "cipd_package": "chromium/testing/weblayer-x86",
               "location": "weblayer_instrumentation_test_M100",
-              "revision": "version:100.0.4896.58"
+              "revision": "version:100.0.4896.60"
             },
             {
               "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
@@ -47560,7 +47560,7 @@
             {
               "cipd_package": "chromium/testing/weblayer-x86",
               "location": "weblayer_instrumentation_test_M101",
-              "revision": "version:101.0.4951.9"
+              "revision": "version:101.0.4951.11"
             },
             {
               "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
@@ -48051,7 +48051,7 @@
             {
               "cipd_package": "chromium/testing/weblayer-x86",
               "location": "weblayer_instrumentation_test_M100",
-              "revision": "version:100.0.4896.58"
+              "revision": "version:100.0.4896.60"
             },
             {
               "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
@@ -48135,7 +48135,7 @@
             {
               "cipd_package": "chromium/testing/weblayer-x86",
               "location": "weblayer_instrumentation_test_M101",
-              "revision": "version:101.0.4951.9"
+              "revision": "version:101.0.4951.11"
             },
             {
               "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
@@ -48555,7 +48555,7 @@
             {
               "cipd_package": "chromium/testing/weblayer-x86",
               "location": "weblayer_instrumentation_test_M100",
-              "revision": "version:100.0.4896.58"
+              "revision": "version:100.0.4896.60"
             },
             {
               "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
@@ -48639,7 +48639,7 @@
             {
               "cipd_package": "chromium/testing/weblayer-x86",
               "location": "weblayer_instrumentation_test_M101",
-              "revision": "version:101.0.4951.9"
+              "revision": "version:101.0.4951.11"
             },
             {
               "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
diff --git a/testing/buildbot/filters/fuchsia.storage_unittests.filter b/testing/buildbot/filters/fuchsia.storage_unittests.filter
index f4a2906..e7798fa 100644
--- a/testing/buildbot/filters/fuchsia.storage_unittests.filter
+++ b/testing/buildbot/filters/fuchsia.storage_unittests.filter
@@ -1,16 +1,3 @@
-# AmountOfFreeDiskSpace not implemented for Fuchsia.
--All/BlobMemoryControllerTest.FullEviction/0
--All/BlobMemoryControllerTest.MultipleFilesPaged/0
--All/BlobMemoryControllerTest.OnMemoryPressure/0
--All/BlobMemoryControllerTest.PageToDisk/0
--All/BlobMemoryControllerTest.PagingStopsWhenFull/0
-
--All/BlobMemoryControllerTest.FullEviction/1
--All/BlobMemoryControllerTest.MultipleFilesPaged/1
--All/BlobMemoryControllerTest.OnMemoryPressure/1
--All/BlobMemoryControllerTest.PageToDisk/1
--All/BlobMemoryControllerTest.PagingStopsWhenFull/1
-
 # crbug.com/1077456
 -All/ObfuscatedFileUtilTest.TestTouch/0
 -DraggedFileUtilTest.TouchTest
diff --git a/testing/buildbot/filters/ozone-linux.interactive_ui_tests_wayland.filter b/testing/buildbot/filters/ozone-linux.interactive_ui_tests_wayland.filter
index 58a7b0c..2aff816 100644
--- a/testing/buildbot/filters/ozone-linux.interactive_ui_tests_wayland.filter
+++ b/testing/buildbot/filters/ozone-linux.interactive_ui_tests_wayland.filter
@@ -42,10 +42,15 @@
 -WidgetInputMethodInteractiveTest.OneWindow
 -WidgetInputMethodInteractiveTest.TwoWindows
 
-# Extremely flaky.
--MenuControllerUITest.TestMouseOverShownMenu
+# TODO(crbug.com/523255,crbug.com/1192997): Fix flaky MenuItemView tests.
+-MenuItemViewTestBasic2.SelectItem2
 -MenuItemViewTestInsert20.InsertItem20
 -MenuItemViewTestInsert00.InsertItem00
+-MenuItemViewTestInsert12.InsertItem12
+-MenuItemViewTestInsert22.InsertItem22
+
+# Extremely flaky.
+-MenuControllerUITest.TestMouseOverShownMenu
 -BookmarkBarViewTest18.BookmarkBarViewTest18_SiblingMenu
 
 # TODO(crbug.com/1195324): flaky fullscreen notification test
diff --git a/testing/buildbot/variants.pyl b/testing/buildbot/variants.pyl
index 4a39a8d2..63a5807 100644
--- a/testing/buildbot/variants.pyl
+++ b/testing/buildbot/variants.pyl
@@ -459,7 +459,7 @@
         {
           'cipd_package': 'chromium/testing/weblayer-x86',
           'location': 'weblayer_instrumentation_test_M101',
-          'revision': 'version:101.0.4951.9',
+          'revision': 'version:101.0.4951.11',
         }
       ],
     },
@@ -483,7 +483,7 @@
         {
           'cipd_package': 'chromium/testing/weblayer-x86',
           'location': 'weblayer_instrumentation_test_M100',
-          'revision': 'version:100.0.4896.58',
+          'revision': 'version:100.0.4896.60',
         }
       ],
     },
@@ -603,7 +603,7 @@
         {
           'cipd_package': 'chromium/testing/weblayer-x86',
           'location': 'weblayer_instrumentation_test_M101',
-          'revision': 'version:101.0.4951.9',
+          'revision': 'version:101.0.4951.11',
         }
       ],
     },
@@ -627,7 +627,7 @@
         {
           'cipd_package': 'chromium/testing/weblayer-x86',
           'location': 'weblayer_instrumentation_test_M100',
-          'revision': 'version:100.0.4896.58',
+          'revision': 'version:100.0.4896.60',
         }
       ],
     },
@@ -747,7 +747,7 @@
         {
           'cipd_package': 'chromium/testing/weblayer-x86',
           'location': 'weblayer_instrumentation_test_M101',
-          'revision': 'version:101.0.4951.9',
+          'revision': 'version:101.0.4951.11',
         }
       ],
     },
@@ -771,7 +771,7 @@
         {
           'cipd_package': 'chromium/testing/weblayer-x86',
           'location': 'weblayer_instrumentation_test_M100',
-          'revision': 'version:100.0.4896.58',
+          'revision': 'version:100.0.4896.60',
         }
       ],
     },
diff --git a/testing/test.gni b/testing/test.gni
index a7682c6..8b24f252 100644
--- a/testing/test.gni
+++ b/testing/test.gni
@@ -971,11 +971,6 @@
 
     data = [ invoker.script ]
 
-    if (defined(invoker.run_under_python2) && invoker.run_under_python2) {
-      use_vpython3 = false
-      data += [ "//.vpython" ]
-    }
-
     if (defined(invoker.data)) {
       data += invoker.data
     }
diff --git a/third_party/blink/common/BUILD.gn b/third_party/blink/common/BUILD.gn
index fc53ff5..8992a17 100644
--- a/third_party/blink/common/BUILD.gn
+++ b/third_party/blink/common/BUILD.gn
@@ -182,6 +182,7 @@
     "notifications/notification_mojom_traits.cc",
     "notifications/notification_resources.cc",
     "notifications/platform_notification_data.cc",
+    "origin_trials/manual_completion_origin_trial_features.cc",
     "origin_trials/navigation_origin_trial_features.cc",
     "origin_trials/trial_token.cc",
     "origin_trials/trial_token_result.cc",
diff --git a/third_party/blink/common/origin_trials/OT_OWNERS b/third_party/blink/common/origin_trials/OT_OWNERS
new file mode 100644
index 0000000..3af6cd4b
--- /dev/null
+++ b/third_party/blink/common/origin_trials/OT_OWNERS
@@ -0,0 +1,7 @@
+# This file covers ownership of the following file:
+#   //third_party/blink/common/origin_trials/manual_completion_origin_trial_features.cc
+
+chasej@chromium.org
+danielrsmith@google.com
+kyleju@chromium.org
+pastithas@google.com
diff --git a/third_party/blink/common/origin_trials/OWNERS b/third_party/blink/common/origin_trials/OWNERS
index 9e491c4..f34e244 100644
--- a/third_party/blink/common/origin_trials/OWNERS
+++ b/third_party/blink/common/origin_trials/OWNERS
@@ -8,5 +8,7 @@
 iclelland@chromium.org
 mek@chromium.org
 
+per-file manual_completion_origin_trial_features.cc=set noparent
+per-file manual_completion_origin_trial_features.cc=file://third_party/blink/common/origin_trials/OT_OWNERS
 per-file navigation_origin_trial_features.cc=set noparent
 per-file navigation_origin_trial_features.cc=file://third_party/blink/SECURITY_OWNERS
diff --git a/third_party/blink/common/origin_trials/manual_completion_origin_trial_features.cc b/third_party/blink/common/origin_trials/manual_completion_origin_trial_features.cc
new file mode 100644
index 0000000..e1ad052
--- /dev/null
+++ b/third_party/blink/common/origin_trials/manual_completion_origin_trial_features.cc
@@ -0,0 +1,25 @@
+// 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.
+
+// This file provides FeatureHasExpiryGracePeriod which is declared in
+// origin_trials.h. FeatureHasExpiryGracePeriod is defined in this file since
+// changes to it require review from the origin trials team, listed in the
+// OWNERS file.
+
+#include "third_party/blink/public/common/origin_trials/origin_trials.h"
+
+#include "base/containers/contains.h"
+
+namespace blink::origin_trials {
+
+bool FeatureHasExpiryGracePeriod(OriginTrialFeature feature) {
+  static OriginTrialFeature const kHasExpiryGracePeriod[] = {
+      // Enable the kOriginTrialsSampleAPIExpiryGracePeriod feature as a manual
+      // completion feature, for tests.
+      OriginTrialFeature::kOriginTrialsSampleAPIExpiryGracePeriod,
+  };
+  return base::Contains(kHasExpiryGracePeriod, feature);
+}
+
+}  // namespace blink::origin_trials
diff --git a/third_party/blink/common/origin_trials/trial_token_validator.cc b/third_party/blink/common/origin_trials/trial_token_validator.cc
index 033711b..746d783 100644
--- a/third_party/blink/common/origin_trials/trial_token_validator.cc
+++ b/third_party/blink/common/origin_trials/trial_token_validator.cc
@@ -12,6 +12,7 @@
 #include "net/http/http_response_headers.h"
 #include "net/url_request/url_request.h"
 #include "third_party/blink/public/common/origin_trials/origin_trial_policy.h"
+#include "third_party/blink/public/common/origin_trials/origin_trials.h"
 #include "third_party/blink/public/common/origin_trials/trial_token.h"
 #include "third_party/blink/public/common/origin_trials/trial_token_result.h"
 
@@ -30,6 +31,30 @@
   return policy && policy->IsOriginTrialsSupported();
 }
 
+// Validates the provided trial_token. If provided, the third_party_origins is
+// only used for validating third-party tokens.
+OriginTrialTokenStatus IsTokenValid(
+    const TrialToken& trial_token,
+    const url::Origin& origin,
+    base::span<const url::Origin> third_party_origins,
+    base::Time current_time) {
+  OriginTrialTokenStatus status;
+  if (trial_token.is_third_party()) {
+    if (!third_party_origins.empty()) {
+      for (const auto& third_party_origin : third_party_origins) {
+        status = trial_token.IsValid(third_party_origin, current_time);
+        if (status == OriginTrialTokenStatus::kSuccess)
+          break;
+      }
+    } else {
+      status = OriginTrialTokenStatus::kWrongOrigin;
+    }
+  } else {
+    status = trial_token.IsValid(origin, current_time);
+  }
+  return status;
+}
+
 }  // namespace
 
 TrialTokenValidator::TrialTokenValidator() {}
@@ -80,20 +105,27 @@
   if (status != OriginTrialTokenStatus::kSuccess)
     return TrialTokenResult(status);
 
-  // If the third_party flag is set on the token, we match it against third
-  // party origin if it exists. Otherwise match against document origin.
-  if (trial_token->is_third_party()) {
-    if (!third_party_origins.empty()) {
-      for (const auto& third_party_origin : third_party_origins) {
-        status = trial_token->IsValid(third_party_origin, current_time);
-        if (status == OriginTrialTokenStatus::kSuccess)
-          break;
+  status =
+      IsTokenValid(*trial_token, origin, third_party_origins, current_time);
+
+  if (status == OriginTrialTokenStatus::kExpired) {
+    if (origin_trials::IsTrialValid(trial_token->feature_name())) {
+      base::Time validated_time = current_time;
+      // Manual completion trials have an expiry grace period. For these trials
+      // the token expiry time is valid if:
+      // token.expiry_time + kExpiryGracePeriod > current_time
+      for (OriginTrialFeature feature :
+           origin_trials::FeaturesForTrial(trial_token->feature_name())) {
+        if (origin_trials::FeatureHasExpiryGracePeriod(feature)) {
+          validated_time = current_time - kExpiryGracePeriod;
+          status = IsTokenValid(*trial_token, origin, third_party_origins,
+                                validated_time);
+          if (status == OriginTrialTokenStatus::kSuccess) {
+            break;
+          }
+        }
       }
-    } else {
-      status = OriginTrialTokenStatus::kWrongOrigin;
     }
-  } else {
-    status = trial_token->IsValid(origin, current_time);
   }
 
   if (status != OriginTrialTokenStatus::kSuccess)
diff --git a/third_party/blink/common/origin_trials/trial_token_validator_unittest.cc b/third_party/blink/common/origin_trials/trial_token_validator_unittest.cc
index b23e23e3..e19c504 100644
--- a/third_party/blink/common/origin_trials/trial_token_validator_unittest.cc
+++ b/third_party/blink/common/origin_trials/trial_token_validator_unittest.cc
@@ -170,6 +170,49 @@
     "MiLCAidXNhZ2UiOiAic3Vic2V0IiwgImZlYXR1cmUiOiAiRnJvYnVsYXRlVGhpcmRQYXJ0eSIs"
     "ICJleHBpcnkiOiAyMDAwMDAwMDAwfQ==";
 
+// Well-formed token for a feature with an expiry grace period.
+// Generate this token with the command (in tools/origin_trials):
+// generate_token.py valid.example.com FrobulateExpiryGracePeriod
+//  --expire-timestamp=2000000000
+const char kExpiryGracePeriodToken[] =
+    "A2AVLsM2Set66KCwTfxH1ni9v8Jcs685qHKDLGam1LmpvnJE9GhYQwbLid3Xlqs/"
+    "2Em2HBp8CMZlj11Qk6R06QUAAABqeyJvcmlnaW4iOiAiaHR0cHM6Ly92YWxpZC5leGFtcGxlLm"
+    "NvbTo0NDMiLCAiZmVhdHVyZSI6ICJGcm9idWxhdGVFeHBpcnlHcmFjZVBlcmlvZCIsICJleHBp"
+    "cnkiOiAyMDAwMDAwMDAwfQ==";
+
+// Well-formed token for match against third party origins and a feature with an
+// expiry grace period.
+// Generate this token with the command (in tools/origin_trials):
+// generate_token.py valid.example.com FrobulateExpiryGracePeriod
+//  --is-third-party --expire-timestamp=2000000000
+const char kExpiryGracePeriodThirdPartyToken[] =
+    "A3wCXDPU5jfARV5KUetX5PI46W41gAbndIZKA7mKrTy6WyXoGFavV+"
+    "vBZejzC2D3Ffti4thz0AOMP+K/"
+    "oWxUvA8AAACAeyJvcmlnaW4iOiAiaHR0cHM6Ly92YWxpZC5leGFtcGxlLmNvbTo0NDMiLCAiZm"
+    "VhdHVyZSI6ICJGcm9idWxhdGVFeHBpcnlHcmFjZVBlcmlvZCIsICJleHBpcnkiOiAyMDAwMDAw"
+    "MDAwLCAiaXNUaGlyZFBhcnR5IjogdHJ1ZX0=";
+
+// Well-formed token, with an unknown feature name.
+// Generate this token with the command (in tools/origin_trials):
+// generate_token.py valid.example.com Grokalyze
+//  --expire-timestamp=2000000000
+const char kUnknownFeatureToken[] =
+    "AxjosEuqWyp9mrBFMOHJtO84YyY4QYuJ6TUNBMVzKMUWPE+B7Nwg2kgZKGO+"
+    "85m0bG0vWEs4m53TWtO1LNf0RgsAAABZeyJvcmlnaW4iOiAiaHR0cHM6Ly92YWxpZC5leGFtcG"
+    "xlLmNvbTo0NDMiLCAiZmVhdHVyZSI6ICJHcm9rYWx5emUiLCAiZXhwaXJ5IjogMjAwMDAwMDAw"
+    "MH0=";
+
+// Well-formed token for match against third party origins, with an unknown
+// feature name. Generate this token with the command (in tools/origin_trials):
+// generate_token.py valid.example.com Grokalyze
+//  --is-third-party --expire-timestamp=2000000000
+const char kUnknownFeatureThirdPartyToken[] =
+    "A7BJkSTbLJ8/EM61BwStBGK3+hAnss/"
+    "fmvpkRmuGuBssyEKczr0iqmj4J3hvRM+"
+    "WzjotyzFopeNLSNU6FGlFZwMAAABveyJvcmlnaW4iOiAiaHR0cHM6Ly92YWxpZC5leGFtcGxlL"
+    "mNvbTo0NDMiLCAiZmVhdHVyZSI6ICJHcm9rYWx5emUiLCAiZXhwaXJ5IjogMjAwMDAwMDAwMCw"
+    "gImlzVGhpcmRQYXJ0eSI6IHRydWV9";
+
 // This timestamp is set to a time after the expiry timestamp of kExpiredToken,
 // but before the expiry timestamp of kValidToken.
 double kNowTimestamp = 1500000000;
@@ -533,4 +576,69 @@
       kAppropriateFeatureName, Now()));
 }
 
+TEST_F(TrialTokenValidatorTest, ValidateValidExpiryGraceToken) {
+  // This token is valid one day before the end of the expiry grace period,
+  // even though it is past the token's expiry time.
+  auto current_time =
+      kSampleTokenExpiryTime + kExpiryGracePeriod - base::Days(1);
+  TrialTokenResult result = validator_.ValidateToken(
+      kExpiryGracePeriodToken, appropriate_origin_, current_time);
+  EXPECT_EQ(result.Status(), blink::OriginTrialTokenStatus::kSuccess);
+  EXPECT_EQ(kSampleTokenExpiryTime, result.ParsedToken()->expiry_time());
+}
+
+TEST_F(TrialTokenValidatorTest, ValidateExpiredExpiryGraceToken) {
+  // This token is expired at the end of the expiry grace period.
+  auto current_time = kSampleTokenExpiryTime + kExpiryGracePeriod;
+  TrialTokenResult result = validator_.ValidateToken(
+      kExpiryGracePeriodToken, appropriate_origin_, current_time);
+  EXPECT_EQ(result.Status(), blink::OriginTrialTokenStatus::kExpired);
+  EXPECT_EQ(kSampleTokenExpiryTime, result.ParsedToken()->expiry_time());
+}
+
+TEST_F(TrialTokenValidatorTest, ValidateValidExpiryGraceThirdPartyToken) {
+  url::Origin third_party_origins[] = {appropriate_origin_};
+  // This token is valid one day before the end of the expiry grace period,
+  // even though it is past the token's expiry time.
+  auto current_time =
+      kSampleTokenExpiryTime + kExpiryGracePeriod - base::Days(1);
+  TrialTokenResult result = validator_.ValidateToken(
+      kExpiryGracePeriodThirdPartyToken, appropriate_origin_,
+      third_party_origins, current_time);
+  EXPECT_EQ(result.Status(), blink::OriginTrialTokenStatus::kSuccess);
+  EXPECT_EQ(kSampleTokenExpiryTime, result.ParsedToken()->expiry_time());
+  EXPECT_EQ(true, result.ParsedToken()->is_third_party());
+}
+
+TEST_F(TrialTokenValidatorTest, ValidateExpiredExpiryGraceThirdPartyToken) {
+  url::Origin third_party_origins[] = {appropriate_origin_};
+  // This token is expired at the end of the expiry grace period.
+  auto current_time = kSampleTokenExpiryTime + kExpiryGracePeriod;
+  TrialTokenResult result = validator_.ValidateToken(
+      kExpiryGracePeriodThirdPartyToken, appropriate_origin_,
+      third_party_origins, current_time);
+  EXPECT_EQ(result.Status(), blink::OriginTrialTokenStatus::kExpired);
+  EXPECT_EQ(kSampleTokenExpiryTime, result.ParsedToken()->expiry_time());
+  EXPECT_EQ(true, result.ParsedToken()->is_third_party());
+}
+
+TEST_F(TrialTokenValidatorTest, ValidateUnknownFeatureToken) {
+  TrialTokenResult result = validator_.ValidateToken(
+      kUnknownFeatureToken, appropriate_origin_, Now());
+  EXPECT_EQ(result.Status(), blink::OriginTrialTokenStatus::kSuccess);
+  EXPECT_EQ(kInappropriateFeatureName, result.ParsedToken()->feature_name());
+  EXPECT_EQ(kSampleTokenExpiryTime, result.ParsedToken()->expiry_time());
+}
+
+TEST_F(TrialTokenValidatorTest, ValidateUnknownFeatureThirdPartyToken) {
+  url::Origin third_party_origins[] = {appropriate_origin_};
+  TrialTokenResult result =
+      validator_.ValidateToken(kUnknownFeatureThirdPartyToken,
+                               appropriate_origin_, third_party_origins, Now());
+  EXPECT_EQ(result.Status(), blink::OriginTrialTokenStatus::kSuccess);
+  EXPECT_EQ(kInappropriateFeatureName, result.ParsedToken()->feature_name());
+  EXPECT_EQ(kSampleTokenExpiryTime, result.ParsedToken()->expiry_time());
+  EXPECT_EQ(true, result.ParsedToken()->is_third_party());
+}
+
 }  // namespace blink::trial_token_validator_unittest
diff --git a/third_party/blink/public/common/origin_trials/origin_trials.h b/third_party/blink/public/common/origin_trials/origin_trials.h
index 0664c51..aa35f10 100644
--- a/third_party/blink/public/common/origin_trials/origin_trials.h
+++ b/third_party/blink/public/common/origin_trials/origin_trials.h
@@ -52,6 +52,10 @@
 BLINK_COMMON_EXPORT bool FeatureEnabledForNavigation(
     OriginTrialFeature feature);
 
+// Returns true if |feature| has an expiry grace period.
+BLINK_COMMON_EXPORT bool FeatureHasExpiryGracePeriod(
+    OriginTrialFeature feature);
+
 }  // namespace origin_trials
 
 }  // namespace blink
diff --git a/third_party/blink/public/common/origin_trials/trial_token_validator.h b/third_party/blink/public/common/origin_trials/trial_token_validator.h
index cabf757..79880d4d 100644
--- a/third_party/blink/public/common/origin_trials/trial_token_validator.h
+++ b/third_party/blink/public/common/origin_trials/trial_token_validator.h
@@ -26,6 +26,9 @@
 class OriginTrialPolicy;
 class TrialTokenResult;
 
+// The expiry grace period for origin trials that must be manually completed.
+constexpr base::TimeDelta kExpiryGracePeriod = base::Days(30);
+
 // TrialTokenValidator checks that a page's OriginTrial token enables a certain
 // feature.
 //
diff --git a/third_party/blink/public/mojom/BUILD.gn b/third_party/blink/public/mojom/BUILD.gn
index 83d68e1..23cc3eea 100644
--- a/third_party/blink/public/mojom/BUILD.gn
+++ b/third_party/blink/public/mojom/BUILD.gn
@@ -102,6 +102,7 @@
     "keyboard_lock/keyboard_lock.mojom",
     "leak_detector/leak_detector.mojom",
     "link_to_text/link_to_text.mojom",
+    "loader/anchor_element_interaction_host.mojom",
     "loader/code_cache.mojom",
     "loader/content_security_notifier.mojom",
     "loader/fetch_client_settings_object.mojom",
diff --git a/third_party/blink/public/mojom/loader/anchor_element_interaction_host.mojom b/third_party/blink/public/mojom/loader/anchor_element_interaction_host.mojom
new file mode 100644
index 0000000..a28eab1
--- /dev/null
+++ b/third_party/blink/public/mojom/loader/anchor_element_interaction_host.mojom
@@ -0,0 +1,16 @@
+// 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 blink.mojom;
+
+import "url/mojom/url.mojom";
+
+// Interface for sending the URL from the renderer side to browser process.
+interface AnchorElementInteractionHost {
+    // On PointerDown events for anchor elements that are part
+    // of the HTTP family, the renderer calls OnPointerDown to pass
+    // the URL to the browser process. In the browser process, the URL gets
+    // preresolved and preconnected.
+    OnPointerDown(url.mojom.Url target);
+};
\ No newline at end of file
diff --git a/third_party/blink/renderer/core/BUILD.gn b/third_party/blink/renderer/core/BUILD.gn
index 5c2f8dc5..6fd98e6 100644
--- a/third_party/blink/renderer/core/BUILD.gn
+++ b/third_party/blink/renderer/core/BUILD.gn
@@ -1282,7 +1282,6 @@
     "fragment_directive/text_fragment_selector_generator_test.cc",
     "fragment_directive/text_fragment_selector_test.cc",
     "frame/ad_tracker_test.cc",
-    "frame/anchor_element_listener_test.cc",
     "frame/attribution_response_parsing_test.cc",
     "frame/attribution_src_loader_test.cc",
     "frame/browser_controls_test.cc",
@@ -1346,6 +1345,7 @@
     "inspector/protocol_unittest.cc",
     "intersection_observer/intersection_observer_test.cc",
     "loader/alternate_signed_exchange_resource_info_test.cc",
+    "loader/anchor_element_interaction_test.cc",
     "loader/base_fetch_context_test.cc",
     "loader/cookie_jar_unittest.cc",
     "loader/document_load_timing_test.cc",
diff --git a/third_party/blink/renderer/core/dom/document.cc b/third_party/blink/renderer/core/dom/document.cc
index e4853bd..79c6959 100644
--- a/third_party/blink/renderer/core/dom/document.cc
+++ b/third_party/blink/renderer/core/dom/document.cc
@@ -249,6 +249,7 @@
 #include "third_party/blink/renderer/core/layout/layout_object_factory.h"
 #include "third_party/blink/renderer/core/layout/layout_view.h"
 #include "third_party/blink/renderer/core/layout/text_autosizer.h"
+#include "third_party/blink/renderer/core/loader/anchor_element_interaction_tracker.h"
 #include "third_party/blink/renderer/core/loader/cookie_jar.h"
 #include "third_party/blink/renderer/core/loader/document_loader.h"
 #include "third_party/blink/renderer/core/loader/frame_fetch_context.h"
@@ -2723,6 +2724,11 @@
     autosizer->UpdatePageInfo();
 
   GetFrame()->DidAttachDocument();
+  if (AnchorElementInteractionTracker::IsFeatureEnabled() &&
+      !GetFrame()->IsProvisional()) {
+    anchor_element_interaction_tracker_ =
+        MakeGarbageCollected<AnchorElementInteractionTracker>(*this);
+  }
   lifecycle_.AdvanceTo(DocumentLifecycle::kStyleClean);
 
   if (View())
@@ -7943,6 +7949,7 @@
   visitor->Trace(meta_theme_color_elements_);
   visitor->Trace(unassociated_listed_elements_);
   visitor->Trace(intrinsic_size_observer_);
+  visitor->Trace(anchor_element_interaction_tracker_);
   Supplementable<Document>::Trace(visitor);
   TreeScope::Trace(visitor);
   ContainerNode::Trace(visitor);
diff --git a/third_party/blink/renderer/core/dom/document.h b/third_party/blink/renderer/core/dom/document.h
index 8bbfac2..ad9038ec 100644
--- a/third_party/blink/renderer/core/dom/document.h
+++ b/third_party/blink/renderer/core/dom/document.h
@@ -110,6 +110,7 @@
 
 namespace blink {
 
+class AnchorElementInteractionTracker;
 class AnimationClock;
 class AXContext;
 class AXObjectCache;
@@ -2119,6 +2120,7 @@
   Member<Element> document_element_;
   UserActionElementSet user_action_elements_;
   Member<RootScrollerController> root_scroller_controller_;
+  Member<AnchorElementInteractionTracker> anchor_element_interaction_tracker_;
 
   double overscroll_accumulated_delta_x_ = 0;
   double overscroll_accumulated_delta_y_ = 0;
diff --git a/third_party/blink/renderer/core/frame/anchor_element_interaction_tracker.cc b/third_party/blink/renderer/core/frame/anchor_element_interaction_tracker.cc
deleted file mode 100644
index 9fd54000..0000000
--- a/third_party/blink/renderer/core/frame/anchor_element_interaction_tracker.cc
+++ /dev/null
@@ -1,29 +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 "third_party/blink/renderer/core/frame/anchor_element_interaction_tracker.h"
-#include "third_party/blink/public/common/features.h"
-#include "third_party/blink/renderer/core/dom/document.h"
-#include "third_party/blink/renderer/core/event_type_names.h"
-#include "third_party/blink/renderer/core/frame/anchor_element_listener.h"
-
-namespace blink {
-
-AnchorElementInteractionTracker::AnchorElementInteractionTracker(
-    Document& document) {
-  anchor_element_listener_ = MakeGarbageCollected<AnchorElementListener>();
-  document.addEventListener(event_type_names::kPointerdown,
-                            anchor_element_listener_, true);
-}
-
-void AnchorElementInteractionTracker::Trace(Visitor* visitor) const {
-  visitor->Trace(anchor_element_listener_);
-}
-
-// static
-bool AnchorElementInteractionTracker::IsFeatureEnabled() {
-  return base::FeatureList::IsEnabled(features::kAnchorElementInteraction);
-}
-
-}  // namespace blink
diff --git a/third_party/blink/renderer/core/frame/anchor_element_listener.h b/third_party/blink/renderer/core/frame/anchor_element_listener.h
deleted file mode 100644
index f2ed820..0000000
--- a/third_party/blink/renderer/core/frame/anchor_element_listener.h
+++ /dev/null
@@ -1,45 +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 THIRD_PARTY_BLINK_RENDERER_CORE_FRAME_ANCHOR_ELEMENT_LISTENER_H_
-#define THIRD_PARTY_BLINK_RENDERER_CORE_FRAME_ANCHOR_ELEMENT_LISTENER_H_
-
-#include "third_party/blink/renderer/core/core_export.h"
-#include "third_party/blink/renderer/core/dom/events/native_event_listener.h"
-
-namespace blink {
-
-class Node;
-class Event;
-class HTMLAnchorElement;
-class KURL;
-
-// Listens for kPointerdown events, and checks to see if an anchor
-// element is clicked with a valid href to be eligible for preloading.
-class CORE_EXPORT AnchorElementListener : public NativeEventListener {
- public:
-  void Invoke(ExecutionContext* execution_context, Event* event) override;
-
- private:
-  HTMLAnchorElement* FirstAnchorElementIncludingSelf(Node* node);
-
-  // Gets the `html_anchor_element's` href attribute if it is part
-  // of the HTTP family
-  KURL GetHrefEligibleForPreloading(
-      const HTMLAnchorElement& html_anchor_element);
-
-  FRIEND_TEST_ALL_PREFIXES(AnchorElementListenerTest, ValidHref);
-  FRIEND_TEST_ALL_PREFIXES(AnchorElementListenerTest, InvalidHref);
-  FRIEND_TEST_ALL_PREFIXES(AnchorElementListenerTest, OneAnchorElementCheck);
-  FRIEND_TEST_ALL_PREFIXES(AnchorElementListenerTest, NestedAnchorElementCheck);
-  FRIEND_TEST_ALL_PREFIXES(AnchorElementListenerTest,
-                           NestedDivAnchorElementCheck);
-  FRIEND_TEST_ALL_PREFIXES(AnchorElementListenerTest,
-                           MultipleNestedAnchorElementCheck);
-  FRIEND_TEST_ALL_PREFIXES(AnchorElementListenerTest, NoAnchorElementCheck);
-};
-
-}  // namespace blink
-
-#endif  // THIRD_PARTY_BLINK_RENDERER_CORE_FRAME_ANCHOR_ELEMENT_LISTENER_H_
diff --git a/third_party/blink/renderer/core/frame/anchor_element_listener_test.cc b/third_party/blink/renderer/core/frame/anchor_element_listener_test.cc
deleted file mode 100644
index d617e9e..0000000
--- a/third_party/blink/renderer/core/frame/anchor_element_listener_test.cc
+++ /dev/null
@@ -1,126 +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 "third_party/blink/renderer/core/frame/anchor_element_listener.h"
-#include "third_party/blink/renderer/core/dom/element.h"
-#include "third_party/blink/renderer/core/html/html_anchor_element.h"
-#include "third_party/blink/renderer/core/testing/sim/sim_request.h"
-#include "third_party/blink/renderer/core/testing/sim/sim_test.h"
-
-namespace blink {
-
-class AnchorElementListenerTest : public SimTest {};
-
-TEST_F(AnchorElementListenerTest, ValidHref) {
-  String source("https://example.com/p1");
-  SimRequest main_resource(source, "text/html");
-  LoadURL(source);
-  main_resource.Complete(
-      "<a id='anchor1' href='https://anchor1.com/'>example</a>");
-  auto* anchor_element_listener_ =
-      MakeGarbageCollected<AnchorElementListener>();
-  auto* anchor_element =
-      DynamicTo<HTMLAnchorElement>(GetDocument().getElementById("anchor1"));
-  KURL URL =
-      anchor_element_listener_->GetHrefEligibleForPreloading(*anchor_element);
-  KURL expected_url = KURL("https://anchor1.com/");
-  EXPECT_FALSE(URL.IsEmpty());
-  EXPECT_EQ(expected_url, URL);
-}
-
-TEST_F(AnchorElementListenerTest, InvalidHref) {
-  String source("https://example.com/p1");
-  SimRequest main_resource(source, "text/html");
-  LoadURL(source);
-  main_resource.Complete("<a id='anchor1' href='about:blank'>example</a>");
-  auto* anchor_element_listener_ =
-      MakeGarbageCollected<AnchorElementListener>();
-  auto* anchor_element =
-      DynamicTo<HTMLAnchorElement>(GetDocument().getElementById("anchor1"));
-  EXPECT_TRUE(
-      anchor_element_listener_->GetHrefEligibleForPreloading(*anchor_element)
-          .IsEmpty());
-}
-
-TEST_F(AnchorElementListenerTest, OneAnchorElementCheck) {
-  String source("https://example.com/p1");
-  SimRequest main_resource(source, "text/html");
-  LoadURL(source);
-  main_resource.Complete(
-      "<a id='anchor1' href='https://anchor1.com/'>example</a>");
-  auto* anchor_element_listener_ =
-      MakeGarbageCollected<AnchorElementListener>();
-  auto* element = GetDocument().getElementById("anchor1");
-  auto* anchor_element =
-      anchor_element_listener_->FirstAnchorElementIncludingSelf(element);
-  auto* expected_anchor =
-      DynamicTo<HTMLAnchorElement>(GetDocument().getElementById("anchor1"));
-  EXPECT_EQ(expected_anchor, anchor_element);
-}
-
-TEST_F(AnchorElementListenerTest, NestedAnchorElementCheck) {
-  String source("https://example.com/p1");
-  SimRequest main_resource(source, "text/html");
-  LoadURL(source);
-  main_resource.Complete(
-      "<a id='anchor1' href='https://anchor1.com/'><a id='anchor2' "
-      "href='https://anchor2.com/'></a></a>");
-  auto* anchor_element_listener_ =
-      MakeGarbageCollected<AnchorElementListener>();
-  auto* element = GetDocument().getElementById("anchor2");
-  auto* anchor_element =
-      anchor_element_listener_->FirstAnchorElementIncludingSelf(element);
-  auto* expected_anchor =
-      DynamicTo<HTMLAnchorElement>(GetDocument().getElementById("anchor2"));
-  EXPECT_EQ(expected_anchor, anchor_element);
-}
-
-TEST_F(AnchorElementListenerTest, NestedDivAnchorElementCheck) {
-  String source("https://example.com/p1");
-  SimRequest main_resource(source, "text/html");
-  LoadURL(source);
-  main_resource.Complete(
-      "<a id='anchor1' href='https://anchor1.com/'><div "
-      "id='div1id'></div></a>");
-  auto* anchor_element_listener_ =
-      MakeGarbageCollected<AnchorElementListener>();
-  auto* element = GetDocument().getElementById("div1id");
-  auto* anchor_element =
-      anchor_element_listener_->FirstAnchorElementIncludingSelf(element);
-  auto* expected_anchor =
-      DynamicTo<HTMLAnchorElement>(GetDocument().getElementById("anchor1"));
-  EXPECT_EQ(expected_anchor, anchor_element);
-}
-
-TEST_F(AnchorElementListenerTest, MultipleNestedAnchorElementCheck) {
-  String source("https://example.com/p1");
-  SimRequest main_resource(source, "text/html");
-  LoadURL(source);
-  main_resource.Complete(
-      "<a id='anchor1' href='https://anchor1.com/'><p id='paragraph1id'><div "
-      "id='div1id'><div id='div2id'></div></div></p></a>");
-  auto* anchor_element_listener_ =
-      MakeGarbageCollected<AnchorElementListener>();
-  auto* element = GetDocument().getElementById("div2id");
-  auto* anchor_element =
-      anchor_element_listener_->FirstAnchorElementIncludingSelf(element);
-  auto* expected_anchor =
-      DynamicTo<HTMLAnchorElement>(GetDocument().getElementById("anchor1"));
-  EXPECT_EQ(expected_anchor, anchor_element);
-}
-
-TEST_F(AnchorElementListenerTest, NoAnchorElementCheck) {
-  String source("https://example.com/p1");
-  SimRequest main_resource(source, "text/html");
-  LoadURL(source);
-  main_resource.Complete("<div id='div1id'></div>");
-  auto* anchor_element_listener_ =
-      MakeGarbageCollected<AnchorElementListener>();
-  auto* element = GetDocument().getElementById("div1id");
-  auto* anchor_element =
-      anchor_element_listener_->FirstAnchorElementIncludingSelf(element);
-  EXPECT_EQ(nullptr, anchor_element);
-}
-
-}  // namespace blink
diff --git a/third_party/blink/renderer/core/frame/build.gni b/third_party/blink/renderer/core/frame/build.gni
index e94c900..00d49ab8 100644
--- a/third_party/blink/renderer/core/frame/build.gni
+++ b/third_party/blink/renderer/core/frame/build.gni
@@ -5,10 +5,6 @@
 blink_core_sources_frame = [
   "ad_tracker.cc",
   "ad_tracker.h",
-  "anchor_element_listener.cc",
-  "anchor_element_listener.h",
-  "anchor_element_interaction_tracker.cc",
-  "anchor_element_interaction_tracker.h",
   "attribution_reporting.cc",
   "attribution_reporting.h",
   "attribution_response_parsing.cc",
diff --git a/third_party/blink/renderer/core/frame/local_frame.cc b/third_party/blink/renderer/core/frame/local_frame.cc
index 85849e4..ae445a52 100644
--- a/third_party/blink/renderer/core/frame/local_frame.cc
+++ b/third_party/blink/renderer/core/frame/local_frame.cc
@@ -109,7 +109,6 @@
 #include "third_party/blink/renderer/core/fileapi/public_url_manager.h"
 #include "third_party/blink/renderer/core/fragment_directive/text_fragment_handler.h"
 #include "third_party/blink/renderer/core/frame/ad_tracker.h"
-#include "third_party/blink/renderer/core/frame/anchor_element_interaction_tracker.h"
 #include "third_party/blink/renderer/core/frame/attribution_src_loader.h"
 #include "third_party/blink/renderer/core/frame/csp/content_security_policy.h"
 #include "third_party/blink/renderer/core/frame/event_handler_registry.h"
@@ -398,7 +397,6 @@
   visitor->Trace(console_);
   visitor->Trace(smooth_scroll_sequencer_);
   visitor->Trace(content_capture_manager_);
-  visitor->Trace(anchor_element_interaction_tracker_);
   visitor->Trace(system_clipboard_);
   visitor->Trace(virtual_keyboard_overlay_changed_observers_);
   visitor->Trace(pause_handle_receivers_);
@@ -711,10 +709,6 @@
   // appendChild()), the drag source will detach and stop firing drag events
   // even after the frame reattaches.
   GetEventHandler().Clear();
-  if (AnchorElementInteractionTracker::IsFeatureEnabled() && !IsProvisional()) {
-    anchor_element_interaction_tracker_ =
-        MakeGarbageCollected<AnchorElementInteractionTracker>(*document);
-  }
   Selection().DidAttachDocument(document);
   notified_color_scheme_ = false;
 }
diff --git a/third_party/blink/renderer/core/frame/local_frame.h b/third_party/blink/renderer/core/frame/local_frame.h
index 3ba2ab1..e2a7640 100644
--- a/third_party/blink/renderer/core/frame/local_frame.h
+++ b/third_party/blink/renderer/core/frame/local_frame.h
@@ -99,7 +99,6 @@
 
 class AdTracker;
 class AttributionSrcLoader;
-class AnchorElementInteractionTracker;
 class AssociatedInterfaceProvider;
 class BrowserInterfaceBrokerProxy;
 class Color;
@@ -876,7 +875,6 @@
   // use the instance owned by their local root.
   Member<SmoothScrollSequencer> smooth_scroll_sequencer_;
   Member<ContentCaptureManager> content_capture_manager_;
-  Member<AnchorElementInteractionTracker> anchor_element_interaction_tracker_;
 
   InterfaceRegistry* const interface_registry_;
 
diff --git a/third_party/blink/renderer/core/html/canvas/predefined_color_space.cc b/third_party/blink/renderer/core/html/canvas/predefined_color_space.cc
index 90deffe..a30ca2a 100644
--- a/third_party/blink/renderer/core/html/canvas/predefined_color_space.cc
+++ b/third_party/blink/renderer/core/html/canvas/predefined_color_space.cc
@@ -48,4 +48,22 @@
   return true;
 }
 
+V8PredefinedColorSpace PredefinedColorSpaceToV8(
+    PredefinedColorSpace color_space) {
+  switch (color_space) {
+    case PredefinedColorSpace::kSRGB:
+      return V8PredefinedColorSpace(V8PredefinedColorSpace::Enum::kSRGB);
+    case PredefinedColorSpace::kRec2020:
+      return V8PredefinedColorSpace(V8PredefinedColorSpace::Enum::kRec2020);
+    case PredefinedColorSpace::kP3:
+      return V8PredefinedColorSpace(V8PredefinedColorSpace::Enum::kDisplayP3);
+    case PredefinedColorSpace::kRec2100HLG:
+      return V8PredefinedColorSpace(V8PredefinedColorSpace::Enum::kRec2100Hlg);
+    case PredefinedColorSpace::kRec2100PQ:
+      return V8PredefinedColorSpace(V8PredefinedColorSpace::Enum::kRec2100Pq);
+    case PredefinedColorSpace::kSRGBLinear:
+      return V8PredefinedColorSpace(V8PredefinedColorSpace::Enum::kSRGBLinear);
+  }
+}
+
 }  // namespace blink
diff --git a/third_party/blink/renderer/core/html/canvas/predefined_color_space.h b/third_party/blink/renderer/core/html/canvas/predefined_color_space.h
index c292994..56a7511 100644
--- a/third_party/blink/renderer/core/html/canvas/predefined_color_space.h
+++ b/third_party/blink/renderer/core/html/canvas/predefined_color_space.h
@@ -25,6 +25,10 @@
                              PredefinedColorSpace& color_space,
                              ExceptionState& exception_state);
 
+// Convert from a PredefinedColorSpace to a V8PredefinedColorSpace.
+V8PredefinedColorSpace CORE_EXPORT
+PredefinedColorSpaceToV8(PredefinedColorSpace color_space);
+
 }  // namespace blink
 
 #endif  // THIRD_PARTY_BLINK_RENDERER_CORE_HTML_CANVAS_PREDEFINED_COLOR_SPACE_H_
diff --git a/third_party/blink/renderer/core/html/fenced_frame/html_fenced_frame_element.cc b/third_party/blink/renderer/core/html/fenced_frame/html_fenced_frame_element.cc
index 03cc743..ac92370 100644
--- a/third_party/blink/renderer/core/html/fenced_frame/html_fenced_frame_element.cc
+++ b/third_party/blink/renderer/core/html/fenced_frame/html_fenced_frame_element.cc
@@ -76,6 +76,8 @@
 }
 
 void HTMLFencedFrameElement::DisconnectContentFrame() {
+  DCHECK(!GetDocument().IsPrerendering());
+
   // The `frame_delegate_` will not exist if the element was not allowed to
   // create its underlying frame at insertion-time.
   if (frame_delegate_)
@@ -108,6 +110,21 @@
   DCHECK(RuntimeEnabledFeatures::FencedFramesEnabled(
       outer_element->GetExecutionContext()));
 
+  // If the element has been disconnected by the time we attempt to create the
+  // delegate (eg, due to deferral while prerendering), we should not create the
+  // delegate.
+  //
+  // NB: this check should remain at the beginning of this function so that the
+  // remainder of the function can safely assume the frame is connected.
+  if (!outer_element->isConnected()) {
+    outer_element->GetDocument().AddConsoleMessage(
+        MakeGarbageCollected<ConsoleMessage>(
+            mojom::blink::ConsoleMessageSource::kJavaScript,
+            mojom::blink::ConsoleMessageLevel::kWarning,
+            "Can't create a fenced frame when disconnected."));
+    return nullptr;
+  }
+
   if (outer_element->GetExecutionContext()->IsSandboxed(
           kFencedFrameMandatoryUnsandboxedFlags)) {
     outer_element->GetDocument().AddConsoleMessage(
@@ -122,8 +139,29 @@
     return nullptr;
   }
 
-  // We know we're not in a detached frame because of the other checks in
-  // `DidNotifySubtreeInsertionsToDocument()`.
+  if (!SubframeLoadingDisabler::CanLoadFrame(*outer_element)) {
+    outer_element->GetDocument().AddConsoleMessage(
+        MakeGarbageCollected<ConsoleMessage>(
+            mojom::blink::ConsoleMessageSource::kJavaScript,
+            mojom::blink::ConsoleMessageLevel::kWarning,
+            "Can't create a fenced frame. Subframe loading disabled."));
+    return nullptr;
+  }
+
+  // The frame limit only needs to be checked on initial creation before
+  // attempting to insert it into the DOM. This behavior matches how iframes
+  // handles frame limits.
+  if (!outer_element->IsCurrentlyWithinFrameLimit()) {
+    outer_element->GetDocument().AddConsoleMessage(
+        MakeGarbageCollected<ConsoleMessage>(
+            mojom::blink::ConsoleMessageSource::kJavaScript,
+            mojom::blink::ConsoleMessageLevel::kWarning,
+            "Can't create a fenced frame. Frame limit exceeded."));
+    return nullptr;
+  }
+
+  // We must be connected at this point due to the isConnected check at the top
+  // of this function.
   DCHECK(outer_element->GetDocument().GetFrame());
   if (Frame* ancestor = outer_element->GetDocument().GetFrame()) {
     mojom::blink::FencedFrameMode current_mode = outer_element->GetMode();
@@ -195,21 +233,7 @@
 }
 
 void HTMLFencedFrameElement::DidNotifySubtreeInsertionsToDocument() {
-  // This method is the only place that sets `frame_delegate_`, and it cannot be
-  // called twice before removal.
-  DCHECK(!frame_delegate_);
-
-  if (!SubframeLoadingDisabler::CanLoadFrame(*this))
-    return;
-
-  // The frame limit only needs to be checked on initial creation before
-  // attempting to insert it into the DOM. This behavior matches how iframes
-  // handles frame limits.
-  if (!IsCurrentlyWithinFrameLimit())
-    return;
-
-  frame_delegate_ = FencedFrameDelegate::Create(this);
-  Navigate();
+  CreateDelegateAndNavigate();
 }
 
 void HTMLFencedFrameElement::RemovedFrom(ContainerNode& node) {
@@ -270,6 +294,13 @@
 void HTMLFencedFrameElement::Navigate() {
   if (!isConnected())
     return;
+
+  // Please see HTMLFencedFrameDelegate::Create for a list of conditions which
+  // could result in not having a frame delegate at this point, one of which is
+  // prerendering. If this function is called while prerendering we won't have a
+  // delegate and will bail early, but this should still be correct since,
+  // post-activation, CreateDelegateAndNavigate will be run which will navigate
+  // to the most current src.
   if (!frame_delegate_)
     return;
 
@@ -300,6 +331,24 @@
     FreezeFrameSize();
 }
 
+void HTMLFencedFrameElement::CreateDelegateAndNavigate() {
+  // We may queue up several calls to CreateDelegateAndNavigate while
+  // prerendering, but we should only actually create the delegate once. Note,
+  // this will also mean that we skip calling Navigate() again, but the result
+  // should still be correct since the first Navigate call will use the
+  // up-to-date src.
+  if (frame_delegate_)
+    return;
+  if (GetDocument().IsPrerendering()) {
+    GetDocument().AddPostPrerenderingActivationStep(
+        WTF::Bind(&HTMLFencedFrameElement::CreateDelegateAndNavigate,
+                  WrapWeakPersistent(this)));
+    return;
+  }
+  frame_delegate_ = FencedFrameDelegate::Create(this);
+  Navigate();
+}
+
 void HTMLFencedFrameElement::AttachLayoutTree(AttachContext& context) {
   HTMLFrameOwnerElement::AttachLayoutTree(context);
   if (features::IsFencedFramesMPArchBased()) {
diff --git a/third_party/blink/renderer/core/html/fenced_frame/html_fenced_frame_element.h b/third_party/blink/renderer/core/html/fenced_frame/html_fenced_frame_element.h
index 8fc36d8..76e7a142 100644
--- a/third_party/blink/renderer/core/html/fenced_frame/html_fenced_frame_element.h
+++ b/third_party/blink/renderer/core/html/fenced_frame/html_fenced_frame_element.h
@@ -106,9 +106,12 @@
 
  private:
   // This method will only navigate the underlying frame if the element
-  // `isConnected()`.
+  // `isConnected()`. It will be deferred if the page is currently prerendering.
   void Navigate();
 
+  // Delegate creation will be deferred if the page is currently prerendering.
+  void CreateDelegateAndNavigate();
+
   // Node overrides.
   Node::InsertionNotificationRequest InsertedInto(ContainerNode&) override;
   void DidNotifySubtreeInsertionsToDocument() override;
diff --git a/third_party/blink/renderer/core/loader/anchor_element_interaction_test.cc b/third_party/blink/renderer/core/loader/anchor_element_interaction_test.cc
new file mode 100644
index 0000000..2082e437
--- /dev/null
+++ b/third_party/blink/renderer/core/loader/anchor_element_interaction_test.cc
@@ -0,0 +1,195 @@
+// 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 <cstddef>
+#include "base/run_loop.h"
+#include "base/test/scoped_feature_list.h"
+#include "mojo/public/cpp/system/message_pipe.h"
+#include "third_party/blink/public/common/features.h"
+#include "third_party/blink/public/mojom/loader/anchor_element_interaction_host.mojom-blink.h"
+#include "third_party/blink/renderer/core/dom/element.h"
+#include "third_party/blink/renderer/core/frame/local_frame.h"
+#include "third_party/blink/renderer/core/html/html_anchor_element.h"
+#include "third_party/blink/renderer/core/loader/anchor_element_interaction_tracker.h"
+#include "third_party/blink/renderer/core/loader/anchor_element_listener.h"
+#include "third_party/blink/renderer/core/testing/sim/sim_request.h"
+#include "third_party/blink/renderer/core/testing/sim/sim_test.h"
+#include "third_party/blink/renderer/platform/testing/testing_platform_support_with_mock_scheduler.h"
+
+namespace blink {
+
+class MockAnchorElementInteractionHost
+    : public mojom::blink::AnchorElementInteractionHost {
+ public:
+  explicit MockAnchorElementInteractionHost(
+      mojo::PendingReceiver<mojom::blink::AnchorElementInteractionHost>
+          pending_receiver) {
+    receiver_.Bind(std::move(pending_receiver));
+  }
+
+  absl::optional<KURL> url_received_ = absl::nullopt;
+
+ private:
+  void OnPointerDown(const KURL& target) override { url_received_ = target; }
+
+ private:
+  mojo::Receiver<mojom::blink::AnchorElementInteractionHost> receiver_{this};
+};
+
+class AnchorElementInteractionTest : public SimTest {
+ public:
+ protected:
+  void SetUp() override {
+    SimTest::SetUp();
+
+    feature_list_.InitAndEnableFeature(features::kAnchorElementInteraction);
+
+    MainFrame().GetFrame()->GetBrowserInterfaceBroker().SetBinderForTesting(
+        mojom::blink::AnchorElementInteractionHost::Name_,
+        WTF::BindRepeating(&AnchorElementInteractionTest::Bind,
+                           WTF::Unretained(this)));
+  }
+
+  void TearDown() override {
+    MainFrame().GetFrame()->GetBrowserInterfaceBroker().SetBinderForTesting(
+        mojom::blink::AnchorElementInteractionHost::Name_, {});
+    hosts_.clear();
+    SimTest::TearDown();
+  }
+
+  void Bind(mojo::ScopedMessagePipeHandle message_pipe_handle) {
+    auto host = std::make_unique<MockAnchorElementInteractionHost>(
+        mojo::PendingReceiver<mojom::blink::AnchorElementInteractionHost>(
+            std::move(message_pipe_handle)));
+    hosts_.push_back(std::move(host));
+  }
+
+  base::test::ScopedFeatureList feature_list_;
+  std::vector<std::unique_ptr<MockAnchorElementInteractionHost>> hosts_;
+};
+
+TEST_F(AnchorElementInteractionTest, InvalidHref) {
+  String source("https://example.com/p1");
+  SimRequest main_resource(source, "text/html");
+  LoadURL(source);
+  main_resource.Complete(R"HTML(
+    <a id='anchor1' href='about:blank'>example</a>
+    <script>
+      const a = document.getElementById('anchor1');
+      var event = new PointerEvent('pointerdown');
+      a.dispatchEvent(event);
+    </script>
+  )HTML");
+  base::RunLoop().RunUntilIdle();
+  absl::optional<KURL> expected_null_url = absl::nullopt;
+  EXPECT_EQ(1u, hosts_.size());
+  absl::optional<KURL> url_received = hosts_[0]->url_received_;
+  EXPECT_FALSE(url_received.has_value());
+  EXPECT_EQ(expected_null_url, url_received);
+}
+
+TEST_F(AnchorElementInteractionTest, NestedAnchorElementCheck) {
+  String source("https://example.com/p1");
+  SimRequest main_resource(source, "text/html");
+  LoadURL(source);
+  main_resource.Complete(R"HTML(
+    <a id='anchor1' href='https://anchor1.com/'><a id='anchor2'
+      href='https://anchor2.com/'></a></a>
+    <script>
+      const a = document.getElementById('anchor2');
+      var event = new PointerEvent('pointerdown');
+      a.dispatchEvent(event);
+    </script>
+  )HTML");
+  base::RunLoop().RunUntilIdle();
+  KURL expected_url = KURL("https://anchor2.com/");
+  EXPECT_EQ(1u, hosts_.size());
+  absl::optional<KURL> url_received = hosts_[0]->url_received_;
+  EXPECT_TRUE(url_received.has_value());
+  EXPECT_EQ(expected_url, url_received);
+}
+
+TEST_F(AnchorElementInteractionTest, NestedDivAnchorElementCheck) {
+  String source("https://example.com/p1");
+  SimRequest main_resource(source, "text/html");
+  LoadURL(source);
+  main_resource.Complete(R"HTML(
+    <a id='anchor1' href='https://anchor1.com/'><div
+      id='div1id'></div></a>
+    <script>
+      const a = document.getElementById('div1id');
+      var event = new PointerEvent('pointerdown');
+      a.dispatchEvent(event);
+    </script>
+  )HTML");
+  base::RunLoop().RunUntilIdle();
+  KURL expected_url = KURL("https://anchor1.com/");
+  EXPECT_EQ(1u, hosts_.size());
+  absl::optional<KURL> url_received = hosts_[0]->url_received_;
+  EXPECT_TRUE(url_received.has_value());
+  EXPECT_EQ(expected_url, url_received);
+}
+
+TEST_F(AnchorElementInteractionTest, MultipleNestedAnchorElementCheck) {
+  String source("https://example.com/p1");
+  SimRequest main_resource(source, "text/html");
+  LoadURL(source);
+  main_resource.Complete(R"HTML(
+    <a id='anchor1' href='https://anchor1.com/'><p id='paragraph1id'><div
+      id='div1id'><div id='div2id'></div></div></p></a>
+    <script>
+      const a = document.getElementById('div2id');
+      var event = new PointerEvent('pointerdown');
+      a.dispatchEvent(event);
+    </script>
+  )HTML");
+  base::RunLoop().RunUntilIdle();
+  KURL expected_url = KURL("https://anchor1.com/");
+  EXPECT_EQ(1u, hosts_.size());
+  absl::optional<KURL> url_received = hosts_[0]->url_received_;
+  EXPECT_TRUE(url_received.has_value());
+  EXPECT_EQ(expected_url, url_received);
+}
+
+TEST_F(AnchorElementInteractionTest, NoAnchorElementCheck) {
+  String source("https://example.com/p1");
+  SimRequest main_resource(source, "text/html");
+  LoadURL(source);
+  main_resource.Complete(R"HTML(
+    <div id='div1id'></div>
+    <script>
+      const a = document.getElementById('div2id');
+      var event = new PointerEvent('pointerdown');
+      a.dispatchEvent(event);
+    </script>
+  )HTML");
+  base::RunLoop().RunUntilIdle();
+  absl::optional<KURL> expected_null_url = absl::nullopt;
+  EXPECT_EQ(1u, hosts_.size());
+  absl::optional<KURL> url_received = hosts_[0]->url_received_;
+  EXPECT_FALSE(url_received.has_value());
+  EXPECT_EQ(expected_null_url, url_received);
+}
+
+TEST_F(AnchorElementInteractionTest, OneAnchorElementCheck) {
+  String source("https://example.com/p1");
+  SimRequest main_resource(source, "text/html");
+  LoadURL(source);
+  main_resource.Complete(R"HTML(
+    <a id="anchor1" href="https://anchor1.com/">foo</a>
+    <script>
+      const a = document.getElementById('anchor1');
+      var event = new PointerEvent('pointerdown');
+      a.dispatchEvent(event);
+    </script>
+  )HTML");
+  base::RunLoop().RunUntilIdle();
+  KURL expected_url = KURL("https://anchor1.com/");
+  EXPECT_EQ(1u, hosts_.size());
+  absl::optional<KURL> url_received = hosts_[0]->url_received_;
+  EXPECT_TRUE(url_received.has_value());
+  EXPECT_EQ(expected_url, url_received);
+}
+
+}  // namespace blink
diff --git a/third_party/blink/renderer/core/loader/anchor_element_interaction_tracker.cc b/third_party/blink/renderer/core/loader/anchor_element_interaction_tracker.cc
new file mode 100644
index 0000000..fb9d0530
--- /dev/null
+++ b/third_party/blink/renderer/core/loader/anchor_element_interaction_tracker.cc
@@ -0,0 +1,52 @@
+// 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 "third_party/blink/renderer/core/loader/anchor_element_interaction_tracker.h"
+#include "base/memory/weak_ptr.h"
+#include "third_party/blink/public/common/browser_interface_broker_proxy.h"
+#include "third_party/blink/public/common/features.h"
+#include "third_party/blink/public/mojom/loader/anchor_element_interaction_host.mojom-blink.h"
+#include "third_party/blink/renderer/core/dom/document.h"
+#include "third_party/blink/renderer/core/event_type_names.h"
+#include "third_party/blink/renderer/core/execution_context/execution_context.h"
+#include "third_party/blink/renderer/core/frame/local_frame.h"
+#include "third_party/blink/renderer/core/loader/anchor_element_listener.h"
+#include "third_party/blink/renderer/platform/heap/persistent.h"
+
+namespace blink {
+
+AnchorElementInteractionTracker::AnchorElementInteractionTracker(
+    Document& document)
+    : interaction_host_(document.GetExecutionContext()) {
+  base::RepeatingCallback<void(const KURL&)> callback =
+      WTF::BindRepeating(&AnchorElementInteractionTracker::OnPointerDown,
+                         WrapWeakPersistent(this));
+
+  anchor_element_listener_ =
+      MakeGarbageCollected<AnchorElementListener>(callback);
+
+  document.addEventListener(event_type_names::kPointerdown,
+                            anchor_element_listener_, true);
+
+  document.GetFrame()->GetBrowserInterfaceBroker().GetInterface(
+      interaction_host_.BindNewPipeAndPassReceiver(
+          document.GetExecutionContext()->GetTaskRunner(
+              TaskType::kInternalDefault)));
+}
+
+void AnchorElementInteractionTracker::Trace(Visitor* visitor) const {
+  visitor->Trace(anchor_element_listener_);
+  visitor->Trace(interaction_host_);
+}
+
+// static
+bool AnchorElementInteractionTracker::IsFeatureEnabled() {
+  return base::FeatureList::IsEnabled(features::kAnchorElementInteraction);
+}
+
+void AnchorElementInteractionTracker::OnPointerDown(const KURL& url) {
+  interaction_host_->OnPointerDown(url);
+}
+
+}  // namespace blink
diff --git a/third_party/blink/renderer/core/frame/anchor_element_interaction_tracker.h b/third_party/blink/renderer/core/loader/anchor_element_interaction_tracker.h
similarity index 66%
rename from third_party/blink/renderer/core/frame/anchor_element_interaction_tracker.h
rename to third_party/blink/renderer/core/loader/anchor_element_interaction_tracker.h
index 4db7633..44d8d09 100644
--- a/third_party/blink/renderer/core/frame/anchor_element_interaction_tracker.h
+++ b/third_party/blink/renderer/core/loader/anchor_element_interaction_tracker.h
@@ -2,16 +2,19 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_FRAME_ANCHOR_ELEMENT_INTERACTION_TRACKER_H_
-#define THIRD_PARTY_BLINK_RENDERER_CORE_FRAME_ANCHOR_ELEMENT_INTERACTION_TRACKER_H_
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_LOADER_ANCHOR_ELEMENT_INTERACTION_TRACKER_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_LOADER_ANCHOR_ELEMENT_INTERACTION_TRACKER_H_
 
+#include "third_party/blink/public/mojom/loader/anchor_element_interaction_host.mojom-blink.h"
 #include "third_party/blink/renderer/platform/heap/garbage_collected.h"
 #include "third_party/blink/renderer/platform/heap/member.h"
+#include "third_party/blink/renderer/platform/mojo/heap_mojo_remote.h"
 
 namespace blink {
 
 class AnchorElementListener;
 class Document;
+class KURL;
 
 // Creates an event listener for mousedown events anywhere on a document.
 // If there is one, the listener will retrieve the valid href from the anchor
@@ -26,12 +29,15 @@
 
   static bool IsFeatureEnabled();
 
+  void OnPointerDown(const KURL& url);
+
   void Trace(Visitor* visitor) const;
 
  private:
   Member<AnchorElementListener> anchor_element_listener_;
+  HeapMojoRemote<mojom::blink::AnchorElementInteractionHost> interaction_host_;
 };
 
 }  // namespace blink
 
-#endif  // THIRD_PARTY_BLINK_RENDERER_CORE_FRAME_ANCHOR_ELEMENT_INTERACTION_TRACKER_H_
+#endif  // THIRD_PARTY_BLINK_RENDERER_CORE_LOADER_ANCHOR_ELEMENT_INTERACTION_TRACKER_H_
diff --git a/third_party/blink/renderer/core/frame/anchor_element_listener.cc b/third_party/blink/renderer/core/loader/anchor_element_listener.cc
similarity index 68%
rename from third_party/blink/renderer/core/frame/anchor_element_listener.cc
rename to third_party/blink/renderer/core/loader/anchor_element_listener.cc
index 5dcc8ef1e..d866b4e7 100644
--- a/third_party/blink/renderer/core/frame/anchor_element_listener.cc
+++ b/third_party/blink/renderer/core/loader/anchor_element_listener.cc
@@ -2,13 +2,23 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#include "third_party/blink/renderer/core/frame/anchor_element_listener.h"
+#include "third_party/blink/renderer/core/loader/anchor_element_listener.h"
 #include "third_party/blink/renderer/core/dom/events/event.h"
+#include "third_party/blink/renderer/core/events/pointer_event.h"
 #include "third_party/blink/renderer/core/html/html_anchor_element.h"
 #include "third_party/blink/renderer/platform/weborigin/kurl.h"
 
+namespace {
+constexpr const int16_t kMainEventButtonValue = 0;
+constexpr const int16_t kAuxiliaryEventButtonValue = 1;
+}  // namespace
+
 namespace blink {
 
+AnchorElementListener::AnchorElementListener(
+    base::RepeatingCallback<void(const KURL&)> callback)
+    : tracker_callback_(std::move(callback)) {}
+
 void AnchorElementListener::Invoke(ExecutionContext* execution_context,
                                    Event* event) {
   if (!event->target()) {
@@ -20,6 +30,11 @@
   if (!event->target()->ToNode()->IsHTMLElement()) {
     return;
   }
+  // TODO(crbug.com/1297312): Check if user changed the default mouse settings
+  if (DynamicTo<PointerEvent>(event)->button() != kMainEventButtonValue &&
+      DynamicTo<PointerEvent>(event)->button() != kAuxiliaryEventButtonValue) {
+    return;
+  }
   Node* node = event->srcElement()->ToNode();
   HTMLAnchorElement* html_anchor_element =
       FirstAnchorElementIncludingSelf(node);
@@ -30,8 +45,7 @@
   if (anchor_url.IsEmpty()) {
     return;
   }
-  // TODO(crbug.com/1297312): send URL back up to the tracker. Tracker will then
-  // communicate with with the browser process to preload.
+  tracker_callback_.Run(anchor_url);
 }
 
 HTMLAnchorElement* AnchorElementListener::FirstAnchorElementIncludingSelf(
diff --git a/third_party/blink/renderer/core/loader/anchor_element_listener.h b/third_party/blink/renderer/core/loader/anchor_element_listener.h
new file mode 100644
index 0000000..dac0832b
--- /dev/null
+++ b/third_party/blink/renderer/core/loader/anchor_element_listener.h
@@ -0,0 +1,42 @@
+// 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 THIRD_PARTY_BLINK_RENDERER_CORE_LOADER_ANCHOR_ELEMENT_LISTENER_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_LOADER_ANCHOR_ELEMENT_LISTENER_H_
+
+#include "base/callback.h"
+#include "third_party/blink/renderer/core/core_export.h"
+#include "third_party/blink/renderer/core/dom/events/native_event_listener.h"
+
+namespace blink {
+
+class Node;
+class Event;
+class HTMLAnchorElement;
+class KURL;
+class ExecutionContext;
+
+// Listens for kPointerdown events, and checks to see if an anchor
+// element is clicked with a valid href to be eligible for preloading.
+class CORE_EXPORT AnchorElementListener : public NativeEventListener {
+ public:
+  explicit AnchorElementListener(
+      base::RepeatingCallback<void(const KURL&)> callback);
+
+  void Invoke(ExecutionContext* execution_context, Event* event) override;
+
+ private:
+  HTMLAnchorElement* FirstAnchorElementIncludingSelf(Node* node);
+
+  // Gets the `html_anchor_element's` href attribute if it is part
+  // of the HTTP family
+  KURL GetHrefEligibleForPreloading(
+      const HTMLAnchorElement& html_anchor_element);
+
+  base::RepeatingCallback<void(const KURL&)> tracker_callback_;
+};
+
+}  // namespace blink
+
+#endif  // THIRD_PARTY_BLINK_RENDERER_CORE_LOADER_ANCHOR_ELEMENT_LISTENER_H_
diff --git a/third_party/blink/renderer/core/loader/build.gni b/third_party/blink/renderer/core/loader/build.gni
index 5be28c5..a0428b0 100644
--- a/third_party/blink/renderer/core/loader/build.gni
+++ b/third_party/blink/renderer/core/loader/build.gni
@@ -5,6 +5,10 @@
 blink_core_sources_loader = [
   "alternate_signed_exchange_resource_info.cc",
   "alternate_signed_exchange_resource_info.h",
+  "anchor_element_listener.cc",
+  "anchor_element_listener.h",
+  "anchor_element_interaction_tracker.cc",
+  "anchor_element_interaction_tracker.h",
   "back_forward_cache_loader_helper_impl.cc",
   "back_forward_cache_loader_helper_impl.h",
   "base_fetch_context.cc",
diff --git a/third_party/blink/renderer/core/paint/theme_painter.cc b/third_party/blink/renderer/core/paint/theme_painter.cc
index 0e3a7ef..60a3ee0e 100644
--- a/third_party/blink/renderer/core/paint/theme_painter.cc
+++ b/third_party/blink/renderer/core/paint/theme_painter.cc
@@ -293,6 +293,9 @@
 
   double min = input->Minimum();
   double max = input->Maximum();
+  if (min >= max)
+    return;
+
   ControlPart part = o.StyleRef().EffectiveAppearance();
   // We don't support ticks on alternate sliders like MediaVolumeSliders.
   if (part != kSliderHorizontalPart && part != kSliderVerticalPart)
diff --git a/third_party/blink/renderer/core/style/computed_style.cc b/third_party/blink/renderer/core/style/computed_style.cc
index 12844458..9982a69 100644
--- a/third_party/blink/renderer/core/style/computed_style.cc
+++ b/third_party/blink/renderer/core/style/computed_style.cc
@@ -2118,18 +2118,10 @@
   if (lh.IsNegative() && GetFont().PrimaryFont())
     return GetFont().PrimaryFont()->GetFontMetrics().LineSpacing();
 
-  if (RuntimeEnabledFeatures::FractionalLineHeightEnabled()) {
-    if (lh.IsPercentOrCalc()) {
-      return MinimumValueForLength(lh, LayoutUnit(ComputedFontSize()));
-    }
+  if (lh.IsPercentOrCalc())
+    return MinimumValueForLength(lh, LayoutUnit(ComputedFontSize()));
 
-    return lh.Value();
-  } else {
-    if (lh.IsPercentOrCalc())
-      return MinimumValueForLength(lh, LayoutUnit(ComputedFontSize())).ToInt();
-
-    return static_cast<int>(std::min(lh.Value(), LayoutUnit::Max().ToFloat()));
-  }
+  return lh.Value();
 }
 
 LayoutUnit ComputedStyle::ComputedLineHeightAsFixed(const Font& font) const {
@@ -2140,20 +2132,10 @@
   if (lh.IsNegative() && font.PrimaryFont())
     return font.PrimaryFont()->GetFontMetrics().FixedLineSpacing();
 
-  if (RuntimeEnabledFeatures::FractionalLineHeightEnabled()) {
-    if (lh.IsPercentOrCalc()) {
-      return MinimumValueForLength(lh, ComputedFontSizeAsFixed(font));
-    }
+  if (lh.IsPercentOrCalc())
+    return MinimumValueForLength(lh, ComputedFontSizeAsFixed(font));
 
-    return LayoutUnit::FromFloatFloor(lh.Value());
-  } else {
-    if (lh.IsPercentOrCalc()) {
-      return LayoutUnit(
-          MinimumValueForLength(lh, ComputedFontSizeAsFixed(font)).ToInt());
-    }
-
-    return LayoutUnit(floorf(lh.Value()));
-  }
+  return LayoutUnit::FromFloatFloor(lh.Value());
 }
 
 LayoutUnit ComputedStyle::ComputedLineHeightAsFixed() const {
diff --git a/third_party/blink/renderer/modules/webgl/webgl_rendering_context_base.cc b/third_party/blink/renderer/modules/webgl/webgl_rendering_context_base.cc
index c65dd76..3274a710 100644
--- a/third_party/blink/renderer/modules/webgl/webgl_rendering_context_base.cc
+++ b/third_party/blink/renderer/modules/webgl/webgl_rendering_context_base.cc
@@ -56,6 +56,7 @@
 #include "third_party/blink/renderer/core/html/canvas/canvas_rendering_context_host.h"
 #include "third_party/blink/renderer/core/html/canvas/html_canvas_element.h"
 #include "third_party/blink/renderer/core/html/canvas/image_data.h"
+#include "third_party/blink/renderer/core/html/canvas/predefined_color_space.h"
 #include "third_party/blink/renderer/core/html/html_image_element.h"
 #include "third_party/blink/renderer/core/html/media/html_video_element.h"
 #include "third_party/blink/renderer/core/imagebitmap/image_bitmap.h"
@@ -1039,7 +1040,7 @@
   // TODO(https://crbug.com/1208480): Move color space to being a read-write
   // attribute instead of a context creation attribute.
   if (RuntimeEnabledFeatures::CanvasColorManagementV2Enabled()) {
-    color_space_ = requested_attributes.color_space;
+    drawing_buffer_color_space_ = requested_attributes.color_space;
     pixel_format_deprecated_ = requested_attributes.pixel_format;
   }
 
@@ -1123,7 +1124,7 @@
       ClampedCanvasSize(), premultiplied_alpha, want_alpha_channel,
       want_depth_buffer, want_stencil_buffer, want_antialiasing, preserve,
       web_gl_version, chromium_image_usage, Host()->FilterQuality(),
-      color_space_, pixel_format_deprecated_,
+      drawing_buffer_color_space_, pixel_format_deprecated_,
       PowerPreferenceToGpuPreference(attrs.power_preference));
 }
 
@@ -1844,13 +1845,40 @@
   return isContextLost() ? 0 : GetDrawingBuffer()->StorageFormat();
 }
 
-V8PredefinedColorSpace WebGLRenderingContextBase::colorSpace() const {
-  return V8PredefinedColorSpace(V8PredefinedColorSpace::Enum::kSRGB);
+V8PredefinedColorSpace WebGLRenderingContextBase::drawingBufferColorSpace()
+    const {
+  return PredefinedColorSpaceToV8(drawing_buffer_color_space_);
 }
 
-void WebGLRenderingContextBase::setColorSpace(
-    const V8PredefinedColorSpace& color_space) const {
+void WebGLRenderingContextBase::setDrawingBufferColorSpace(
+    const V8PredefinedColorSpace& v8_color_space,
+    ExceptionState& exception_state) {
+  // Some values for PredefinedColorSpace are supposed to be guarded behind
+  // runtime flags. Use `ValidateAndConvertColorSpace` to throw an exception if
+  // `v8_color_space` should not be exposed.
+  PredefinedColorSpace color_space = PredefinedColorSpace::kSRGB;
+  if (!ValidateAndConvertColorSpace(v8_color_space, color_space,
+                                    exception_state)) {
+    return;
+  }
   NOTIMPLEMENTED();
+  drawing_buffer_color_space_ = color_space;
+}
+
+V8PredefinedColorSpace WebGLRenderingContextBase::unpackColorSpace() const {
+  return PredefinedColorSpaceToV8(unpack_color_space_);
+}
+
+void WebGLRenderingContextBase::setUnpackColorSpace(
+    const V8PredefinedColorSpace& v8_color_space,
+    ExceptionState& exception_state) {
+  PredefinedColorSpace color_space = PredefinedColorSpace::kSRGB;
+  if (!ValidateAndConvertColorSpace(v8_color_space, color_space,
+                                    exception_state)) {
+    return;
+  }
+  NOTIMPLEMENTED();
+  unpack_color_space_ = color_space;
 }
 
 void WebGLRenderingContextBase::activeTexture(GLenum texture) {
@@ -5221,9 +5249,9 @@
   // have been intentional.
   const SkAlphaType alpha_type =
       CreationAttributes().alpha ? kPremul_SkAlphaType : kOpaque_SkAlphaType;
-  return SkColorInfo(CanvasPixelFormatToSkColorType(pixel_format_deprecated_),
-                     alpha_type,
-                     PredefinedColorSpaceToSkColorSpace(color_space_));
+  return SkColorInfo(
+      CanvasPixelFormatToSkColorType(pixel_format_deprecated_), alpha_type,
+      PredefinedColorSpaceToSkColorSpace(drawing_buffer_color_space_));
 }
 
 gfx::Rect WebGLRenderingContextBase::GetImageDataSize(ImageData* pixels) {
diff --git a/third_party/blink/renderer/modules/webgl/webgl_rendering_context_base.h b/third_party/blink/renderer/modules/webgl/webgl_rendering_context_base.h
index 7328aeb..4ae837d 100644
--- a/third_party/blink/renderer/modules/webgl/webgl_rendering_context_base.h
+++ b/third_party/blink/renderer/modules/webgl/webgl_rendering_context_base.h
@@ -176,8 +176,13 @@
   int drawingBufferWidth() const;
   int drawingBufferHeight() const;
   GLenum drawingBufferFormat() const;
-  V8PredefinedColorSpace colorSpace() const;
-  void setColorSpace(const V8PredefinedColorSpace& color_space) const;
+  V8PredefinedColorSpace drawingBufferColorSpace() const;
+  void setDrawingBufferColorSpace(const V8PredefinedColorSpace& color_space,
+                                  ExceptionState&);
+
+  V8PredefinedColorSpace unpackColorSpace() const;
+  void setUnpackColorSpace(const V8PredefinedColorSpace& color_space,
+                           ExceptionState&);
 
   void activeTexture(GLenum texture);
   void attachShader(WebGLProgram*, WebGLShader*);
@@ -1918,7 +1923,10 @@
 
   bool has_been_drawn_to_ = false;
 
-  PredefinedColorSpace color_space_ = PredefinedColorSpace::kSRGB;
+  PredefinedColorSpace drawing_buffer_color_space_ =
+      PredefinedColorSpace::kSRGB;
+  PredefinedColorSpace unpack_color_space_ = PredefinedColorSpace::kSRGB;
+
   // The pixel format of the WebGL canvas. This is based on a deprecated
   // specification that is being replaced by drawingBufferStorage.
   CanvasPixelFormat pixel_format_deprecated_ = CanvasPixelFormat::kUint8;
diff --git a/third_party/blink/renderer/modules/webgl/webgl_rendering_context_base.idl b/third_party/blink/renderer/modules/webgl/webgl_rendering_context_base.idl
index 4afb970..85e395d 100644
--- a/third_party/blink/renderer/modules/webgl/webgl_rendering_context_base.idl
+++ b/third_party/blink/renderer/modules/webgl/webgl_rendering_context_base.idl
@@ -465,7 +465,8 @@
     readonly attribute GLsizei drawingBufferWidth;
     readonly attribute GLsizei drawingBufferHeight;
     [RuntimeEnabled=CanvasColorManagementV2] readonly attribute GLenum drawingBufferFormat;
-    [RuntimeEnabled=CanvasColorManagementV2] attribute PredefinedColorSpace colorSpace;
+    [RaisesException=Setter, RuntimeEnabled=CanvasColorManagementV2] attribute PredefinedColorSpace drawingBufferColorSpace;
+    [RaisesException=Setter, RuntimeEnabled=CanvasColorManagementV2] attribute PredefinedColorSpace unpackColorSpace;
 
     void activeTexture(GLenum texture);
     void attachShader(WebGLProgram program, WebGLShader shader);
diff --git a/third_party/blink/renderer/platform/runtime_enabled_features.json5 b/third_party/blink/renderer/platform/runtime_enabled_features.json5
index 28458b75..6b8041b 100644
--- a/third_party/blink/renderer/platform/runtime_enabled_features.json5
+++ b/third_party/blink/renderer/platform/runtime_enabled_features.json5
@@ -1202,10 +1202,6 @@
       status: {"ChromeOS_Ash": "stable", "ChromeOS_Lacros": "stable"},
     },
     {
-      name: "FractionalLineHeight",
-      status: "stable",
-    },
-    {
       name: "FractionalScrollOffsets",
       status: "experimental",
     },
@@ -1726,6 +1722,12 @@
     // As above. Do not change this flag to stable, as it exists solely to
     // generate code used by the origin trials sample API implementation.
     {
+      name: "OriginTrialsSampleAPIExpiryGracePeriod",
+      origin_trial_feature_name: "FrobulateExpiryGracePeriod",
+    },
+    // As above. Do not change this flag to stable, as it exists solely to
+    // generate code used by the origin trials sample API implementation.
+    {
       name: "OriginTrialsSampleAPIImplied",
       origin_trial_feature_name: "FrobulateImplied",
       implied_by: ["OriginTrialsSampleAPI", "OriginTrialsSampleAPIInvalidOS"],
diff --git a/third_party/blink/web_tests/TestExpectations b/third_party/blink/web_tests/TestExpectations
index bccd58c..e36daf2 100644
--- a/third_party/blink/web_tests/TestExpectations
+++ b/third_party/blink/web_tests/TestExpectations
@@ -7616,3 +7616,7 @@
 # Sheriff 2022-03-24
 crbug.com/1309756 http/tests/images/force-reload.html [ Skip ]
 crbug.com/1309756 [ Mac11-arm64 ] http/tests/images/force-reload-image-document.html [ Skip ]
+
+# Sheriff 2022-03-25
+crbug.com/1197296 [ Linux ] virtual/unified-autoplay/external/wpt/feature-policy/feature-policy-frame-policy-timing.https.sub.html [ Failure Pass ]
+crbug.com/1197296 [ Linux ] external/wpt/feature-policy/feature-policy-frame-policy-timing.https.sub.html [ Failure Pass ]
diff --git a/third_party/blink/web_tests/external/wpt/html/semantics/forms/the-input-element/invalid-datalist-options-crash.html b/third_party/blink/web_tests/external/wpt/html/semantics/forms/the-input-element/invalid-datalist-options-crash.html
new file mode 100644
index 0000000..7cdd551
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/html/semantics/forms/the-input-element/invalid-datalist-options-crash.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<link rel="help" href="https://html.spec.whatwg.org/multipage/#the-input-element">
+<input max="0" list="ticks" type="range">
+<datalist id="ticks">
+  <option value="0"></option>
+</datalist>
diff --git a/third_party/blink/web_tests/http/tests/serviceworker/webexposed/global-interface-listing-service-worker-expected.txt b/third_party/blink/web_tests/http/tests/serviceworker/webexposed/global-interface-listing-service-worker-expected.txt
index b176815..4563af4 100644
--- a/third_party/blink/web_tests/http/tests/serviceworker/webexposed/global-interface-listing-service-worker-expected.txt
+++ b/third_party/blink/web_tests/http/tests/serviceworker/webexposed/global-interface-listing-service-worker-expected.txt
@@ -2184,10 +2184,11 @@
     attribute WAIT_FAILED
     attribute ZERO
     getter canvas
-    getter colorSpace
+    getter drawingBufferColorSpace
     getter drawingBufferFormat
     getter drawingBufferHeight
     getter drawingBufferWidth
+    getter unpackColorSpace
     method activeTexture
     method attachShader
     method beginQuery
@@ -2416,7 +2417,8 @@
     method vertexAttribPointer
     method viewport
     method waitSync
-    setter colorSpace
+    setter drawingBufferColorSpace
+    setter unpackColorSpace
 interface WebGLActiveInfo
     attribute @@toStringTag
     getter name
@@ -2737,10 +2739,11 @@
     attribute VIEWPORT
     attribute ZERO
     getter canvas
-    getter colorSpace
+    getter drawingBufferColorSpace
     getter drawingBufferFormat
     getter drawingBufferHeight
     getter drawingBufferWidth
+    getter unpackColorSpace
     method activeTexture
     method attachShader
     method bindAttribLocation
@@ -2881,7 +2884,8 @@
     method vertexAttrib4fv
     method vertexAttribPointer
     method viewport
-    setter colorSpace
+    setter drawingBufferColorSpace
+    setter unpackColorSpace
 interface WebGLSampler
     attribute @@toStringTag
     method constructor
diff --git a/third_party/blink/web_tests/webexposed/global-interface-listing-dedicated-worker-expected.txt b/third_party/blink/web_tests/webexposed/global-interface-listing-dedicated-worker-expected.txt
index 0d3585a..3e219074 100644
--- a/third_party/blink/web_tests/webexposed/global-interface-listing-dedicated-worker-expected.txt
+++ b/third_party/blink/web_tests/webexposed/global-interface-listing-dedicated-worker-expected.txt
@@ -2423,10 +2423,11 @@
 [Worker]     attribute WAIT_FAILED
 [Worker]     attribute ZERO
 [Worker]     getter canvas
-[Worker]     getter colorSpace
+[Worker]     getter drawingBufferColorSpace
 [Worker]     getter drawingBufferFormat
 [Worker]     getter drawingBufferHeight
 [Worker]     getter drawingBufferWidth
+[Worker]     getter unpackColorSpace
 [Worker]     method activeTexture
 [Worker]     method attachShader
 [Worker]     method beginQuery
@@ -2655,7 +2656,8 @@
 [Worker]     method vertexAttribPointer
 [Worker]     method viewport
 [Worker]     method waitSync
-[Worker]     setter colorSpace
+[Worker]     setter drawingBufferColorSpace
+[Worker]     setter unpackColorSpace
 [Worker] interface WebGLActiveInfo
 [Worker]     attribute @@toStringTag
 [Worker]     getter name
@@ -2976,10 +2978,11 @@
 [Worker]     attribute VIEWPORT
 [Worker]     attribute ZERO
 [Worker]     getter canvas
-[Worker]     getter colorSpace
+[Worker]     getter drawingBufferColorSpace
 [Worker]     getter drawingBufferFormat
 [Worker]     getter drawingBufferHeight
 [Worker]     getter drawingBufferWidth
+[Worker]     getter unpackColorSpace
 [Worker]     method activeTexture
 [Worker]     method attachShader
 [Worker]     method bindAttribLocation
@@ -3120,7 +3123,8 @@
 [Worker]     method vertexAttrib4fv
 [Worker]     method vertexAttribPointer
 [Worker]     method viewport
-[Worker]     setter colorSpace
+[Worker]     setter drawingBufferColorSpace
+[Worker]     setter unpackColorSpace
 [Worker] interface WebGLSampler
 [Worker]     attribute @@toStringTag
 [Worker]     method constructor
diff --git a/third_party/blink/web_tests/webexposed/global-interface-listing-expected.txt b/third_party/blink/web_tests/webexposed/global-interface-listing-expected.txt
index 153ddda5..690509d 100644
--- a/third_party/blink/web_tests/webexposed/global-interface-listing-expected.txt
+++ b/third_party/blink/web_tests/webexposed/global-interface-listing-expected.txt
@@ -9931,10 +9931,11 @@
     attribute WAIT_FAILED
     attribute ZERO
     getter canvas
-    getter colorSpace
+    getter drawingBufferColorSpace
     getter drawingBufferFormat
     getter drawingBufferHeight
     getter drawingBufferWidth
+    getter unpackColorSpace
     method activeTexture
     method attachShader
     method beginQuery
@@ -10163,7 +10164,8 @@
     method vertexAttribPointer
     method viewport
     method waitSync
-    setter colorSpace
+    setter drawingBufferColorSpace
+    setter unpackColorSpace
 interface WebGLActiveInfo
     attribute @@toStringTag
     getter name
@@ -10488,10 +10490,11 @@
     attribute VIEWPORT
     attribute ZERO
     getter canvas
-    getter colorSpace
+    getter drawingBufferColorSpace
     getter drawingBufferFormat
     getter drawingBufferHeight
     getter drawingBufferWidth
+    getter unpackColorSpace
     method activeTexture
     method attachShader
     method bindAttribLocation
@@ -10632,7 +10635,8 @@
     method vertexAttrib4fv
     method vertexAttribPointer
     method viewport
-    setter colorSpace
+    setter drawingBufferColorSpace
+    setter unpackColorSpace
 interface WebGLSampler
     attribute @@toStringTag
     method constructor
diff --git a/third_party/blink/web_tests/webexposed/global-interface-listing-shared-worker-expected.txt b/third_party/blink/web_tests/webexposed/global-interface-listing-shared-worker-expected.txt
index ff89e8ec..793699fe 100644
--- a/third_party/blink/web_tests/webexposed/global-interface-listing-shared-worker-expected.txt
+++ b/third_party/blink/web_tests/webexposed/global-interface-listing-shared-worker-expected.txt
@@ -2063,10 +2063,11 @@
 [Worker]     attribute WAIT_FAILED
 [Worker]     attribute ZERO
 [Worker]     getter canvas
-[Worker]     getter colorSpace
+[Worker]     getter drawingBufferColorSpace
 [Worker]     getter drawingBufferFormat
 [Worker]     getter drawingBufferHeight
 [Worker]     getter drawingBufferWidth
+[Worker]     getter unpackColorSpace
 [Worker]     method activeTexture
 [Worker]     method attachShader
 [Worker]     method beginQuery
@@ -2295,7 +2296,8 @@
 [Worker]     method vertexAttribPointer
 [Worker]     method viewport
 [Worker]     method waitSync
-[Worker]     setter colorSpace
+[Worker]     setter drawingBufferColorSpace
+[Worker]     setter unpackColorSpace
 [Worker] interface WebGLActiveInfo
 [Worker]     attribute @@toStringTag
 [Worker]     getter name
@@ -2616,10 +2618,11 @@
 [Worker]     attribute VIEWPORT
 [Worker]     attribute ZERO
 [Worker]     getter canvas
-[Worker]     getter colorSpace
+[Worker]     getter drawingBufferColorSpace
 [Worker]     getter drawingBufferFormat
 [Worker]     getter drawingBufferHeight
 [Worker]     getter drawingBufferWidth
+[Worker]     getter unpackColorSpace
 [Worker]     method activeTexture
 [Worker]     method attachShader
 [Worker]     method bindAttribLocation
@@ -2760,7 +2763,8 @@
 [Worker]     method vertexAttrib4fv
 [Worker]     method vertexAttribPointer
 [Worker]     method viewport
-[Worker]     setter colorSpace
+[Worker]     setter drawingBufferColorSpace
+[Worker]     setter unpackColorSpace
 [Worker] interface WebGLSampler
 [Worker]     attribute @@toStringTag
 [Worker]     method constructor
diff --git a/third_party/blink/web_tests/wpt_internal/fenced_frame/csp-urn.https.html b/third_party/blink/web_tests/wpt_internal/fenced_frame/csp-urn.https.html
index a066e5c..9222ca8 100644
--- a/third_party/blink/web_tests/wpt_internal/fenced_frame/csp-urn.https.html
+++ b/third_party/blink/web_tests/wpt_internal/fenced_frame/csp-urn.https.html
@@ -20,7 +20,7 @@
     setupCSP(csp);
 
     const key = token();
-    attachFencedFrame(await generateURN("resources/embeddee.html", [key]));
+    attachFencedFrame(await generateURN("resources/embeddee.html", [key]), "opaque-ads");
 
     const result = await nextValueFromServer(key);
     assert_equals(result, "PASS",
@@ -40,7 +40,7 @@
       writeValueToServer(key, e.violatedDirective + ";" + e.blockedURI);
     });
 
-    attachFencedFrame(await generateURN("resources/embeddee.html", [key]));
+    attachFencedFrame(await generateURN("resources/embeddee.html", [key]), "opaque-ads");
 
     const result = await nextValueFromServer(key);
     assert_equals(result, "fenced-frame-src;",
diff --git a/third_party/blink/web_tests/wpt_internal/fenced_frame/resources/utils.js b/third_party/blink/web_tests/wpt_internal/fenced_frame/resources/utils.js
index 42aa0ec..827be18 100644
--- a/third_party/blink/web_tests/wpt_internal/fenced_frame/resources/utils.js
+++ b/third_party/blink/web_tests/wpt_internal/fenced_frame/resources/utils.js
@@ -187,13 +187,16 @@
   return digest_slices.join('-');
 }
 
-function attachFencedFrame(url) {
+function attachFencedFrame(url, mode='') {
   assert_implements(
       window.HTMLFencedFrameElement,
       'The HTMLFencedFrameElement should be exposed on the window object');
 
   const fenced_frame = document.createElement('fencedframe');
   assert_true('mode' in fenced_frame);
+  if (mode) {
+    fenced_frame.mode = mode;
+  }
   fenced_frame.src = url;
   document.body.append(fenced_frame);
   return fenced_frame;
diff --git a/third_party/blink/web_tests/wpt_internal/webgpu/cts.https.html b/third_party/blink/web_tests/wpt_internal/webgpu/cts.https.html
index 62c44aab..f1c299f 100644
--- a/third_party/blink/web_tests/wpt_internal/webgpu/cts.https.html
+++ b/third_party/blink/web_tests/wpt_internal/webgpu/cts.https.html
@@ -2614,8 +2614,10 @@
 <meta name=variant content='?q=webgpu:api,validation,state,device_lost,destroy:command,renderPass,draw:*'>
 <meta name=variant content='?q=webgpu:api,validation,state,device_lost,destroy:command,renderPass,renderBundle:*'>
 <meta name=variant content='?q=webgpu:api,validation,state,device_lost,destroy:queue,writeBuffer:*'>
-<meta name=variant content='?q=webgpu:api,validation,state,device_lost,destroy:queue,writeTexture:*'>
-<meta name=variant content='?q=webgpu:api,validation,state,device_lost,destroy:queue,copyExternalImageToTexture:*'>
+<meta name=variant content='?q=webgpu:api,validation,state,device_lost,destroy:queue,writeTexture,2d,uncompressed_format:*'>
+<meta name=variant content='?q=webgpu:api,validation,state,device_lost,destroy:queue,writeTexture,2d,compressed_format:*'>
+<meta name=variant content='?q=webgpu:api,validation,state,device_lost,destroy:queue,copyExternalImageToTexture,canvas:*'>
+<meta name=variant content='?q=webgpu:api,validation,state,device_lost,destroy:queue,copyExternalImageToTexture,imageBitmap:*'>
 <meta name=variant content='?q=webgpu:api,validation,texture,destroy:base:*'>
 <meta name=variant content='?q=webgpu:api,validation,texture,destroy:twice:*'>
 <meta name=variant content='?q=webgpu:api,validation,texture,destroy:submit_a_destroyed_texture_as_attachment:*'>
diff --git a/third_party/libaom/README.chromium b/third_party/libaom/README.chromium
index bcf117c..6b403c5 100644
--- a/third_party/libaom/README.chromium
+++ b/third_party/libaom/README.chromium
@@ -3,9 +3,9 @@
 URL: https://aomedia.googlesource.com/aom/
 Version: 3.1.2
 CPEPrefix: cpe:/a:aomedia:aomedia:3.1.2
-Date: Wednesday March 09 2022
+Date: Thursday March 17 2022
 Branch: main
-Commit: ee1ed1ccf2b9ecedd6aee438eafc7cc61c23342d
+Commit: 24fa287e152b319d8998e24c0f174f4043138bfd
 License: BSD
 License File: source/libaom/LICENSE
 Security Critical: yes
diff --git a/third_party/libaom/libaom_srcs.gni b/third_party/libaom/libaom_srcs.gni
index 7403ec3..c0c95aef 100644
--- a/third_party/libaom/libaom_srcs.gni
+++ b/third_party/libaom/libaom_srcs.gni
@@ -371,6 +371,10 @@
 aom_av1_rc_qmode_sources = [
   "//third_party/libaom/source/libaom/av1/ratectrl_qmode.cc",
   "//third_party/libaom/source/libaom/av1/ratectrl_qmode.h",
+  "//third_party/libaom/source/libaom/av1/ratectrl_qmode_interface.cc",
+  "//third_party/libaom/source/libaom/av1/ratectrl_qmode_interface.h",
+  "//third_party/libaom/source/libaom/av1/reference_manager.cc",
+  "//third_party/libaom/source/libaom/av1/reference_manager.h",
 ]
 
 aom_dsp_common_asm_sse2 = [
@@ -712,6 +716,9 @@
   "//third_party/libaom/source/libaom/common/webmenc.h",
 ]
 
+av1_rc_qmode_sources =
+    [ "//third_party/libaom/source/libaom/test/ratectrl_qmode_test.cc" ]
+
 # Files below this line are generated by the libaom build system.
 
 aom_rtcd_sources_gen = [
diff --git a/third_party/libaom/source/config/config/aom_version.h b/third_party/libaom/source/config/config/aom_version.h
index 94c7ef0..50ea908 100644
--- a/third_party/libaom/source/config/config/aom_version.h
+++ b/third_party/libaom/source/config/config/aom_version.h
@@ -12,8 +12,8 @@
 #define VERSION_MAJOR 3
 #define VERSION_MINOR 3
 #define VERSION_PATCH 0
-#define VERSION_EXTRA "330-gee1ed1ccf"
+#define VERSION_EXTRA "364-g24fa287e1"
 #define VERSION_PACKED \
   ((VERSION_MAJOR << 16) | (VERSION_MINOR << 8) | (VERSION_PATCH))
-#define VERSION_STRING_NOSP "3.3.0-330-gee1ed1ccf"
-#define VERSION_STRING " 3.3.0-330-gee1ed1ccf"
+#define VERSION_STRING_NOSP "3.3.0-364-g24fa287e1"
+#define VERSION_STRING " 3.3.0-364-g24fa287e1"
diff --git a/third_party/libaom/source/config/ios/arm-neon/config/aom_config.asm b/third_party/libaom/source/config/ios/arm-neon/config/aom_config.asm
index 078e78c..cd9dfe35 100644
--- a/third_party/libaom/source/config/ios/arm-neon/config/aom_config.asm
+++ b/third_party/libaom/source/config/ios/arm-neon/config/aom_config.asm
@@ -51,6 +51,7 @@
 CONFIG_OS_SUPPORT equ 1
 CONFIG_PARTITION_SEARCH_ORDER equ 0
 CONFIG_PIC equ 0
+CONFIG_RATECTRL_LOG equ 0
 CONFIG_RD_COMMAND equ 0
 CONFIG_RD_DEBUG equ 0
 CONFIG_REALTIME_ONLY equ 1
diff --git a/third_party/libaom/source/config/ios/arm-neon/config/aom_config.h b/third_party/libaom/source/config/ios/arm-neon/config/aom_config.h
index c456f0a..9a0f04e2 100644
--- a/third_party/libaom/source/config/ios/arm-neon/config/aom_config.h
+++ b/third_party/libaom/source/config/ios/arm-neon/config/aom_config.h
@@ -53,6 +53,7 @@
 #define CONFIG_OS_SUPPORT 1
 #define CONFIG_PARTITION_SEARCH_ORDER 0
 #define CONFIG_PIC 0
+#define CONFIG_RATECTRL_LOG 0
 #define CONFIG_RD_COMMAND 0
 #define CONFIG_RD_DEBUG 0
 #define CONFIG_REALTIME_ONLY 1
diff --git a/third_party/libaom/source/config/ios/arm-neon/config/aom_dsp_rtcd.h b/third_party/libaom/source/config/ios/arm-neon/config/aom_dsp_rtcd.h
index 01c7d9b..a1644504 100644
--- a/third_party/libaom/source/config/ios/arm-neon/config/aom_dsp_rtcd.h
+++ b/third_party/libaom/source/config/ios/arm-neon/config/aom_dsp_rtcd.h
@@ -984,6 +984,20 @@
 unsigned int aom_get_mb_ss_c(const int16_t*);
 #define aom_get_mb_ss aom_get_mb_ss_c
 
+void aom_get_sse_sum_8x8_quad_c(const uint8_t* src_ptr,
+                                int source_stride,
+                                const uint8_t* ref_ptr,
+                                int ref_stride,
+                                unsigned int* sse,
+                                int* sum);
+void aom_get_sse_sum_8x8_quad_neon(const uint8_t* src_ptr,
+                                   int source_stride,
+                                   const uint8_t* ref_ptr,
+                                   int ref_stride,
+                                   unsigned int* sse,
+                                   int* sum);
+#define aom_get_sse_sum_8x8_quad aom_get_sse_sum_8x8_quad_neon
+
 void aom_h_predictor_16x16_c(uint8_t* dst,
                              ptrdiff_t y_stride,
                              const uint8_t* above,
diff --git a/third_party/libaom/source/config/ios/arm64/config/aom_config.asm b/third_party/libaom/source/config/ios/arm64/config/aom_config.asm
index 078e78c..cd9dfe35 100644
--- a/third_party/libaom/source/config/ios/arm64/config/aom_config.asm
+++ b/third_party/libaom/source/config/ios/arm64/config/aom_config.asm
@@ -51,6 +51,7 @@
 CONFIG_OS_SUPPORT equ 1
 CONFIG_PARTITION_SEARCH_ORDER equ 0
 CONFIG_PIC equ 0
+CONFIG_RATECTRL_LOG equ 0
 CONFIG_RD_COMMAND equ 0
 CONFIG_RD_DEBUG equ 0
 CONFIG_REALTIME_ONLY equ 1
diff --git a/third_party/libaom/source/config/ios/arm64/config/aom_config.h b/third_party/libaom/source/config/ios/arm64/config/aom_config.h
index c456f0a..9a0f04e2 100644
--- a/third_party/libaom/source/config/ios/arm64/config/aom_config.h
+++ b/third_party/libaom/source/config/ios/arm64/config/aom_config.h
@@ -53,6 +53,7 @@
 #define CONFIG_OS_SUPPORT 1
 #define CONFIG_PARTITION_SEARCH_ORDER 0
 #define CONFIG_PIC 0
+#define CONFIG_RATECTRL_LOG 0
 #define CONFIG_RD_COMMAND 0
 #define CONFIG_RD_DEBUG 0
 #define CONFIG_REALTIME_ONLY 1
diff --git a/third_party/libaom/source/config/ios/arm64/config/aom_dsp_rtcd.h b/third_party/libaom/source/config/ios/arm64/config/aom_dsp_rtcd.h
index 01c7d9b..a1644504 100644
--- a/third_party/libaom/source/config/ios/arm64/config/aom_dsp_rtcd.h
+++ b/third_party/libaom/source/config/ios/arm64/config/aom_dsp_rtcd.h
@@ -984,6 +984,20 @@
 unsigned int aom_get_mb_ss_c(const int16_t*);
 #define aom_get_mb_ss aom_get_mb_ss_c
 
+void aom_get_sse_sum_8x8_quad_c(const uint8_t* src_ptr,
+                                int source_stride,
+                                const uint8_t* ref_ptr,
+                                int ref_stride,
+                                unsigned int* sse,
+                                int* sum);
+void aom_get_sse_sum_8x8_quad_neon(const uint8_t* src_ptr,
+                                   int source_stride,
+                                   const uint8_t* ref_ptr,
+                                   int ref_stride,
+                                   unsigned int* sse,
+                                   int* sum);
+#define aom_get_sse_sum_8x8_quad aom_get_sse_sum_8x8_quad_neon
+
 void aom_h_predictor_16x16_c(uint8_t* dst,
                              ptrdiff_t y_stride,
                              const uint8_t* above,
diff --git a/third_party/libaom/source/config/linux/arm-neon-cpu-detect/config/aom_config.asm b/third_party/libaom/source/config/linux/arm-neon-cpu-detect/config/aom_config.asm
index eed135e5..78170e3 100644
--- a/third_party/libaom/source/config/linux/arm-neon-cpu-detect/config/aom_config.asm
+++ b/third_party/libaom/source/config/linux/arm-neon-cpu-detect/config/aom_config.asm
@@ -51,6 +51,7 @@
 CONFIG_OS_SUPPORT equ 1
 CONFIG_PARTITION_SEARCH_ORDER equ 0
 CONFIG_PIC equ 0
+CONFIG_RATECTRL_LOG equ 0
 CONFIG_RD_COMMAND equ 0
 CONFIG_RD_DEBUG equ 0
 CONFIG_REALTIME_ONLY equ 1
diff --git a/third_party/libaom/source/config/linux/arm-neon-cpu-detect/config/aom_config.h b/third_party/libaom/source/config/linux/arm-neon-cpu-detect/config/aom_config.h
index 1c9749c0..8e2471d 100644
--- a/third_party/libaom/source/config/linux/arm-neon-cpu-detect/config/aom_config.h
+++ b/third_party/libaom/source/config/linux/arm-neon-cpu-detect/config/aom_config.h
@@ -53,6 +53,7 @@
 #define CONFIG_OS_SUPPORT 1
 #define CONFIG_PARTITION_SEARCH_ORDER 0
 #define CONFIG_PIC 0
+#define CONFIG_RATECTRL_LOG 0
 #define CONFIG_RD_COMMAND 0
 #define CONFIG_RD_DEBUG 0
 #define CONFIG_REALTIME_ONLY 1
diff --git a/third_party/libaom/source/config/linux/arm-neon-cpu-detect/config/aom_dsp_rtcd.h b/third_party/libaom/source/config/linux/arm-neon-cpu-detect/config/aom_dsp_rtcd.h
index ce7ef6a..22f37d2 100644
--- a/third_party/libaom/source/config/linux/arm-neon-cpu-detect/config/aom_dsp_rtcd.h
+++ b/third_party/libaom/source/config/linux/arm-neon-cpu-detect/config/aom_dsp_rtcd.h
@@ -1072,6 +1072,25 @@
 unsigned int aom_get_mb_ss_c(const int16_t*);
 #define aom_get_mb_ss aom_get_mb_ss_c
 
+void aom_get_sse_sum_8x8_quad_c(const uint8_t* src_ptr,
+                                int source_stride,
+                                const uint8_t* ref_ptr,
+                                int ref_stride,
+                                unsigned int* sse,
+                                int* sum);
+void aom_get_sse_sum_8x8_quad_neon(const uint8_t* src_ptr,
+                                   int source_stride,
+                                   const uint8_t* ref_ptr,
+                                   int ref_stride,
+                                   unsigned int* sse,
+                                   int* sum);
+RTCD_EXTERN void (*aom_get_sse_sum_8x8_quad)(const uint8_t* src_ptr,
+                                             int source_stride,
+                                             const uint8_t* ref_ptr,
+                                             int ref_stride,
+                                             unsigned int* sse,
+                                             int* sum);
+
 void aom_h_predictor_16x16_c(uint8_t* dst,
                              ptrdiff_t y_stride,
                              const uint8_t* above,
@@ -4958,6 +4977,9 @@
   aom_get8x8var = aom_get8x8var_c;
   if (flags & HAS_NEON)
     aom_get8x8var = aom_get8x8var_neon;
+  aom_get_sse_sum_8x8_quad = aom_get_sse_sum_8x8_quad_c;
+  if (flags & HAS_NEON)
+    aom_get_sse_sum_8x8_quad = aom_get_sse_sum_8x8_quad_neon;
   aom_h_predictor_16x16 = aom_h_predictor_16x16_c;
   if (flags & HAS_NEON)
     aom_h_predictor_16x16 = aom_h_predictor_16x16_neon;
diff --git a/third_party/libaom/source/config/linux/arm-neon/config/aom_config.asm b/third_party/libaom/source/config/linux/arm-neon/config/aom_config.asm
index 078e78c..cd9dfe35 100644
--- a/third_party/libaom/source/config/linux/arm-neon/config/aom_config.asm
+++ b/third_party/libaom/source/config/linux/arm-neon/config/aom_config.asm
@@ -51,6 +51,7 @@
 CONFIG_OS_SUPPORT equ 1
 CONFIG_PARTITION_SEARCH_ORDER equ 0
 CONFIG_PIC equ 0
+CONFIG_RATECTRL_LOG equ 0
 CONFIG_RD_COMMAND equ 0
 CONFIG_RD_DEBUG equ 0
 CONFIG_REALTIME_ONLY equ 1
diff --git a/third_party/libaom/source/config/linux/arm-neon/config/aom_config.h b/third_party/libaom/source/config/linux/arm-neon/config/aom_config.h
index c456f0a..9a0f04e2 100644
--- a/third_party/libaom/source/config/linux/arm-neon/config/aom_config.h
+++ b/third_party/libaom/source/config/linux/arm-neon/config/aom_config.h
@@ -53,6 +53,7 @@
 #define CONFIG_OS_SUPPORT 1
 #define CONFIG_PARTITION_SEARCH_ORDER 0
 #define CONFIG_PIC 0
+#define CONFIG_RATECTRL_LOG 0
 #define CONFIG_RD_COMMAND 0
 #define CONFIG_RD_DEBUG 0
 #define CONFIG_REALTIME_ONLY 1
diff --git a/third_party/libaom/source/config/linux/arm-neon/config/aom_dsp_rtcd.h b/third_party/libaom/source/config/linux/arm-neon/config/aom_dsp_rtcd.h
index 01c7d9b..a1644504 100644
--- a/third_party/libaom/source/config/linux/arm-neon/config/aom_dsp_rtcd.h
+++ b/third_party/libaom/source/config/linux/arm-neon/config/aom_dsp_rtcd.h
@@ -984,6 +984,20 @@
 unsigned int aom_get_mb_ss_c(const int16_t*);
 #define aom_get_mb_ss aom_get_mb_ss_c
 
+void aom_get_sse_sum_8x8_quad_c(const uint8_t* src_ptr,
+                                int source_stride,
+                                const uint8_t* ref_ptr,
+                                int ref_stride,
+                                unsigned int* sse,
+                                int* sum);
+void aom_get_sse_sum_8x8_quad_neon(const uint8_t* src_ptr,
+                                   int source_stride,
+                                   const uint8_t* ref_ptr,
+                                   int ref_stride,
+                                   unsigned int* sse,
+                                   int* sum);
+#define aom_get_sse_sum_8x8_quad aom_get_sse_sum_8x8_quad_neon
+
 void aom_h_predictor_16x16_c(uint8_t* dst,
                              ptrdiff_t y_stride,
                              const uint8_t* above,
diff --git a/third_party/libaom/source/config/linux/arm/config/aom_config.asm b/third_party/libaom/source/config/linux/arm/config/aom_config.asm
index 51451f7..6dfa8a6 100644
--- a/third_party/libaom/source/config/linux/arm/config/aom_config.asm
+++ b/third_party/libaom/source/config/linux/arm/config/aom_config.asm
@@ -51,6 +51,7 @@
 CONFIG_OS_SUPPORT equ 1
 CONFIG_PARTITION_SEARCH_ORDER equ 0
 CONFIG_PIC equ 0
+CONFIG_RATECTRL_LOG equ 0
 CONFIG_RD_COMMAND equ 0
 CONFIG_RD_DEBUG equ 0
 CONFIG_REALTIME_ONLY equ 1
diff --git a/third_party/libaom/source/config/linux/arm/config/aom_config.h b/third_party/libaom/source/config/linux/arm/config/aom_config.h
index 3469ff8f..fb2fc3da 100644
--- a/third_party/libaom/source/config/linux/arm/config/aom_config.h
+++ b/third_party/libaom/source/config/linux/arm/config/aom_config.h
@@ -53,6 +53,7 @@
 #define CONFIG_OS_SUPPORT 1
 #define CONFIG_PARTITION_SEARCH_ORDER 0
 #define CONFIG_PIC 0
+#define CONFIG_RATECTRL_LOG 0
 #define CONFIG_RD_COMMAND 0
 #define CONFIG_RD_DEBUG 0
 #define CONFIG_REALTIME_ONLY 1
diff --git a/third_party/libaom/source/config/linux/arm/config/aom_dsp_rtcd.h b/third_party/libaom/source/config/linux/arm/config/aom_dsp_rtcd.h
index 5dd7d22..b1599c4 100644
--- a/third_party/libaom/source/config/linux/arm/config/aom_dsp_rtcd.h
+++ b/third_party/libaom/source/config/linux/arm/config/aom_dsp_rtcd.h
@@ -875,6 +875,14 @@
 unsigned int aom_get_mb_ss_c(const int16_t*);
 #define aom_get_mb_ss aom_get_mb_ss_c
 
+void aom_get_sse_sum_8x8_quad_c(const uint8_t* src_ptr,
+                                int source_stride,
+                                const uint8_t* ref_ptr,
+                                int ref_stride,
+                                unsigned int* sse,
+                                int* sum);
+#define aom_get_sse_sum_8x8_quad aom_get_sse_sum_8x8_quad_c
+
 void aom_h_predictor_16x16_c(uint8_t* dst,
                              ptrdiff_t y_stride,
                              const uint8_t* above,
diff --git a/third_party/libaom/source/config/linux/arm64/config/aom_config.asm b/third_party/libaom/source/config/linux/arm64/config/aom_config.asm
index 078e78c..cd9dfe35 100644
--- a/third_party/libaom/source/config/linux/arm64/config/aom_config.asm
+++ b/third_party/libaom/source/config/linux/arm64/config/aom_config.asm
@@ -51,6 +51,7 @@
 CONFIG_OS_SUPPORT equ 1
 CONFIG_PARTITION_SEARCH_ORDER equ 0
 CONFIG_PIC equ 0
+CONFIG_RATECTRL_LOG equ 0
 CONFIG_RD_COMMAND equ 0
 CONFIG_RD_DEBUG equ 0
 CONFIG_REALTIME_ONLY equ 1
diff --git a/third_party/libaom/source/config/linux/arm64/config/aom_config.h b/third_party/libaom/source/config/linux/arm64/config/aom_config.h
index c456f0a..9a0f04e2 100644
--- a/third_party/libaom/source/config/linux/arm64/config/aom_config.h
+++ b/third_party/libaom/source/config/linux/arm64/config/aom_config.h
@@ -53,6 +53,7 @@
 #define CONFIG_OS_SUPPORT 1
 #define CONFIG_PARTITION_SEARCH_ORDER 0
 #define CONFIG_PIC 0
+#define CONFIG_RATECTRL_LOG 0
 #define CONFIG_RD_COMMAND 0
 #define CONFIG_RD_DEBUG 0
 #define CONFIG_REALTIME_ONLY 1
diff --git a/third_party/libaom/source/config/linux/arm64/config/aom_dsp_rtcd.h b/third_party/libaom/source/config/linux/arm64/config/aom_dsp_rtcd.h
index 01c7d9b..a1644504 100644
--- a/third_party/libaom/source/config/linux/arm64/config/aom_dsp_rtcd.h
+++ b/third_party/libaom/source/config/linux/arm64/config/aom_dsp_rtcd.h
@@ -984,6 +984,20 @@
 unsigned int aom_get_mb_ss_c(const int16_t*);
 #define aom_get_mb_ss aom_get_mb_ss_c
 
+void aom_get_sse_sum_8x8_quad_c(const uint8_t* src_ptr,
+                                int source_stride,
+                                const uint8_t* ref_ptr,
+                                int ref_stride,
+                                unsigned int* sse,
+                                int* sum);
+void aom_get_sse_sum_8x8_quad_neon(const uint8_t* src_ptr,
+                                   int source_stride,
+                                   const uint8_t* ref_ptr,
+                                   int ref_stride,
+                                   unsigned int* sse,
+                                   int* sum);
+#define aom_get_sse_sum_8x8_quad aom_get_sse_sum_8x8_quad_neon
+
 void aom_h_predictor_16x16_c(uint8_t* dst,
                              ptrdiff_t y_stride,
                              const uint8_t* above,
diff --git a/third_party/libaom/source/config/linux/generic/config/aom_config.asm b/third_party/libaom/source/config/linux/generic/config/aom_config.asm
index 7febc43..446cdfe 100644
--- a/third_party/libaom/source/config/linux/generic/config/aom_config.asm
+++ b/third_party/libaom/source/config/linux/generic/config/aom_config.asm
@@ -51,6 +51,7 @@
 CONFIG_OS_SUPPORT equ 1
 CONFIG_PARTITION_SEARCH_ORDER equ 0
 CONFIG_PIC equ 0
+CONFIG_RATECTRL_LOG equ 0
 CONFIG_RD_COMMAND equ 0
 CONFIG_RD_DEBUG equ 0
 CONFIG_REALTIME_ONLY equ 1
diff --git a/third_party/libaom/source/config/linux/generic/config/aom_config.h b/third_party/libaom/source/config/linux/generic/config/aom_config.h
index 89f409e..6a54b6e 100644
--- a/third_party/libaom/source/config/linux/generic/config/aom_config.h
+++ b/third_party/libaom/source/config/linux/generic/config/aom_config.h
@@ -53,6 +53,7 @@
 #define CONFIG_OS_SUPPORT 1
 #define CONFIG_PARTITION_SEARCH_ORDER 0
 #define CONFIG_PIC 0
+#define CONFIG_RATECTRL_LOG 0
 #define CONFIG_RD_COMMAND 0
 #define CONFIG_RD_DEBUG 0
 #define CONFIG_REALTIME_ONLY 1
diff --git a/third_party/libaom/source/config/linux/generic/config/aom_dsp_rtcd.h b/third_party/libaom/source/config/linux/generic/config/aom_dsp_rtcd.h
index 1fb294f..5113ef62 100644
--- a/third_party/libaom/source/config/linux/generic/config/aom_dsp_rtcd.h
+++ b/third_party/libaom/source/config/linux/generic/config/aom_dsp_rtcd.h
@@ -875,6 +875,14 @@
 unsigned int aom_get_mb_ss_c(const int16_t*);
 #define aom_get_mb_ss aom_get_mb_ss_c
 
+void aom_get_sse_sum_8x8_quad_c(const uint8_t* src_ptr,
+                                int source_stride,
+                                const uint8_t* ref_ptr,
+                                int ref_stride,
+                                unsigned int* sse,
+                                int* sum);
+#define aom_get_sse_sum_8x8_quad aom_get_sse_sum_8x8_quad_c
+
 void aom_h_predictor_16x16_c(uint8_t* dst,
                              ptrdiff_t y_stride,
                              const uint8_t* above,
diff --git a/third_party/libaom/source/config/linux/ia32/config/aom_config.asm b/third_party/libaom/source/config/linux/ia32/config/aom_config.asm
index 16afb81..992f14d 100644
--- a/third_party/libaom/source/config/linux/ia32/config/aom_config.asm
+++ b/third_party/libaom/source/config/linux/ia32/config/aom_config.asm
@@ -41,6 +41,7 @@
 %define CONFIG_OS_SUPPORT 1
 %define CONFIG_PARTITION_SEARCH_ORDER 0
 %define CONFIG_PIC 1
+%define CONFIG_RATECTRL_LOG 0
 %define CONFIG_RD_COMMAND 0
 %define CONFIG_RD_DEBUG 0
 %define CONFIG_REALTIME_ONLY 1
diff --git a/third_party/libaom/source/config/linux/ia32/config/aom_config.h b/third_party/libaom/source/config/linux/ia32/config/aom_config.h
index df532d0..6677fab 100644
--- a/third_party/libaom/source/config/linux/ia32/config/aom_config.h
+++ b/third_party/libaom/source/config/linux/ia32/config/aom_config.h
@@ -53,6 +53,7 @@
 #define CONFIG_OS_SUPPORT 1
 #define CONFIG_PARTITION_SEARCH_ORDER 0
 #define CONFIG_PIC 1
+#define CONFIG_RATECTRL_LOG 0
 #define CONFIG_RD_COMMAND 0
 #define CONFIG_RD_DEBUG 0
 #define CONFIG_REALTIME_ONLY 1
diff --git a/third_party/libaom/source/config/linux/ia32/config/aom_dsp_rtcd.h b/third_party/libaom/source/config/linux/ia32/config/aom_dsp_rtcd.h
index ceef0144..9c802bc2 100644
--- a/third_party/libaom/source/config/linux/ia32/config/aom_dsp_rtcd.h
+++ b/third_party/libaom/source/config/linux/ia32/config/aom_dsp_rtcd.h
@@ -1979,6 +1979,31 @@
 unsigned int aom_get_mb_ss_sse2(const int16_t*);
 #define aom_get_mb_ss aom_get_mb_ss_sse2
 
+void aom_get_sse_sum_8x8_quad_c(const uint8_t* src_ptr,
+                                int source_stride,
+                                const uint8_t* ref_ptr,
+                                int ref_stride,
+                                unsigned int* sse,
+                                int* sum);
+void aom_get_sse_sum_8x8_quad_sse2(const uint8_t* src_ptr,
+                                   int source_stride,
+                                   const uint8_t* ref_ptr,
+                                   int ref_stride,
+                                   unsigned int* sse,
+                                   int* sum);
+void aom_get_sse_sum_8x8_quad_avx2(const uint8_t* src_ptr,
+                                   int source_stride,
+                                   const uint8_t* ref_ptr,
+                                   int ref_stride,
+                                   unsigned int* sse,
+                                   int* sum);
+RTCD_EXTERN void (*aom_get_sse_sum_8x8_quad)(const uint8_t* src_ptr,
+                                             int source_stride,
+                                             const uint8_t* ref_ptr,
+                                             int ref_stride,
+                                             unsigned int* sse,
+                                             int* sum);
+
 void aom_h_predictor_16x16_c(uint8_t* dst,
                              ptrdiff_t y_stride,
                              const uint8_t* above,
@@ -8612,6 +8637,9 @@
   aom_get_blk_sse_sum = aom_get_blk_sse_sum_sse2;
   if (flags & HAS_AVX2)
     aom_get_blk_sse_sum = aom_get_blk_sse_sum_avx2;
+  aom_get_sse_sum_8x8_quad = aom_get_sse_sum_8x8_quad_sse2;
+  if (flags & HAS_AVX2)
+    aom_get_sse_sum_8x8_quad = aom_get_sse_sum_8x8_quad_avx2;
   aom_h_predictor_32x32 = aom_h_predictor_32x32_sse2;
   if (flags & HAS_AVX2)
     aom_h_predictor_32x32 = aom_h_predictor_32x32_avx2;
diff --git a/third_party/libaom/source/config/linux/x64/config/aom_config.asm b/third_party/libaom/source/config/linux/x64/config/aom_config.asm
index 676c3dd8..b3a9b81 100644
--- a/third_party/libaom/source/config/linux/x64/config/aom_config.asm
+++ b/third_party/libaom/source/config/linux/x64/config/aom_config.asm
@@ -41,6 +41,7 @@
 %define CONFIG_OS_SUPPORT 1
 %define CONFIG_PARTITION_SEARCH_ORDER 0
 %define CONFIG_PIC 0
+%define CONFIG_RATECTRL_LOG 0
 %define CONFIG_RD_COMMAND 0
 %define CONFIG_RD_DEBUG 0
 %define CONFIG_REALTIME_ONLY 1
diff --git a/third_party/libaom/source/config/linux/x64/config/aom_config.h b/third_party/libaom/source/config/linux/x64/config/aom_config.h
index cb7e41a..63e5091 100644
--- a/third_party/libaom/source/config/linux/x64/config/aom_config.h
+++ b/third_party/libaom/source/config/linux/x64/config/aom_config.h
@@ -53,6 +53,7 @@
 #define CONFIG_OS_SUPPORT 1
 #define CONFIG_PARTITION_SEARCH_ORDER 0
 #define CONFIG_PIC 0
+#define CONFIG_RATECTRL_LOG 0
 #define CONFIG_RD_COMMAND 0
 #define CONFIG_RD_DEBUG 0
 #define CONFIG_REALTIME_ONLY 1
diff --git a/third_party/libaom/source/config/linux/x64/config/aom_dsp_rtcd.h b/third_party/libaom/source/config/linux/x64/config/aom_dsp_rtcd.h
index 94a1db4..eed2759 100644
--- a/third_party/libaom/source/config/linux/x64/config/aom_dsp_rtcd.h
+++ b/third_party/libaom/source/config/linux/x64/config/aom_dsp_rtcd.h
@@ -1982,6 +1982,31 @@
 unsigned int aom_get_mb_ss_sse2(const int16_t*);
 #define aom_get_mb_ss aom_get_mb_ss_sse2
 
+void aom_get_sse_sum_8x8_quad_c(const uint8_t* src_ptr,
+                                int source_stride,
+                                const uint8_t* ref_ptr,
+                                int ref_stride,
+                                unsigned int* sse,
+                                int* sum);
+void aom_get_sse_sum_8x8_quad_sse2(const uint8_t* src_ptr,
+                                   int source_stride,
+                                   const uint8_t* ref_ptr,
+                                   int ref_stride,
+                                   unsigned int* sse,
+                                   int* sum);
+void aom_get_sse_sum_8x8_quad_avx2(const uint8_t* src_ptr,
+                                   int source_stride,
+                                   const uint8_t* ref_ptr,
+                                   int ref_stride,
+                                   unsigned int* sse,
+                                   int* sum);
+RTCD_EXTERN void (*aom_get_sse_sum_8x8_quad)(const uint8_t* src_ptr,
+                                             int source_stride,
+                                             const uint8_t* ref_ptr,
+                                             int ref_stride,
+                                             unsigned int* sse,
+                                             int* sum);
+
 void aom_h_predictor_16x16_c(uint8_t* dst,
                              ptrdiff_t y_stride,
                              const uint8_t* above,
@@ -8651,6 +8676,9 @@
   aom_get_blk_sse_sum = aom_get_blk_sse_sum_sse2;
   if (flags & HAS_AVX2)
     aom_get_blk_sse_sum = aom_get_blk_sse_sum_avx2;
+  aom_get_sse_sum_8x8_quad = aom_get_sse_sum_8x8_quad_sse2;
+  if (flags & HAS_AVX2)
+    aom_get_sse_sum_8x8_quad = aom_get_sse_sum_8x8_quad_avx2;
   aom_h_predictor_32x32 = aom_h_predictor_32x32_sse2;
   if (flags & HAS_AVX2)
     aom_h_predictor_32x32 = aom_h_predictor_32x32_avx2;
diff --git a/third_party/libaom/source/config/win/arm64/config/aom_config.asm b/third_party/libaom/source/config/win/arm64/config/aom_config.asm
index 078e78c..cd9dfe35 100644
--- a/third_party/libaom/source/config/win/arm64/config/aom_config.asm
+++ b/third_party/libaom/source/config/win/arm64/config/aom_config.asm
@@ -51,6 +51,7 @@
 CONFIG_OS_SUPPORT equ 1
 CONFIG_PARTITION_SEARCH_ORDER equ 0
 CONFIG_PIC equ 0
+CONFIG_RATECTRL_LOG equ 0
 CONFIG_RD_COMMAND equ 0
 CONFIG_RD_DEBUG equ 0
 CONFIG_REALTIME_ONLY equ 1
diff --git a/third_party/libaom/source/config/win/arm64/config/aom_config.h b/third_party/libaom/source/config/win/arm64/config/aom_config.h
index a3d119e..6ffa438 100644
--- a/third_party/libaom/source/config/win/arm64/config/aom_config.h
+++ b/third_party/libaom/source/config/win/arm64/config/aom_config.h
@@ -53,6 +53,7 @@
 #define CONFIG_OS_SUPPORT 1
 #define CONFIG_PARTITION_SEARCH_ORDER 0
 #define CONFIG_PIC 0
+#define CONFIG_RATECTRL_LOG 0
 #define CONFIG_RD_COMMAND 0
 #define CONFIG_RD_DEBUG 0
 #define CONFIG_REALTIME_ONLY 1
diff --git a/third_party/libaom/source/config/win/arm64/config/aom_dsp_rtcd.h b/third_party/libaom/source/config/win/arm64/config/aom_dsp_rtcd.h
index 01c7d9b..a1644504 100644
--- a/third_party/libaom/source/config/win/arm64/config/aom_dsp_rtcd.h
+++ b/third_party/libaom/source/config/win/arm64/config/aom_dsp_rtcd.h
@@ -984,6 +984,20 @@
 unsigned int aom_get_mb_ss_c(const int16_t*);
 #define aom_get_mb_ss aom_get_mb_ss_c
 
+void aom_get_sse_sum_8x8_quad_c(const uint8_t* src_ptr,
+                                int source_stride,
+                                const uint8_t* ref_ptr,
+                                int ref_stride,
+                                unsigned int* sse,
+                                int* sum);
+void aom_get_sse_sum_8x8_quad_neon(const uint8_t* src_ptr,
+                                   int source_stride,
+                                   const uint8_t* ref_ptr,
+                                   int ref_stride,
+                                   unsigned int* sse,
+                                   int* sum);
+#define aom_get_sse_sum_8x8_quad aom_get_sse_sum_8x8_quad_neon
+
 void aom_h_predictor_16x16_c(uint8_t* dst,
                              ptrdiff_t y_stride,
                              const uint8_t* above,
diff --git a/third_party/libaom/source/config/win/ia32/config/aom_config.asm b/third_party/libaom/source/config/win/ia32/config/aom_config.asm
index 71d7db2..f687b613c 100644
--- a/third_party/libaom/source/config/win/ia32/config/aom_config.asm
+++ b/third_party/libaom/source/config/win/ia32/config/aom_config.asm
@@ -41,6 +41,7 @@
 %define CONFIG_OS_SUPPORT 1
 %define CONFIG_PARTITION_SEARCH_ORDER 0
 %define CONFIG_PIC 1
+%define CONFIG_RATECTRL_LOG 0
 %define CONFIG_RD_COMMAND 0
 %define CONFIG_RD_DEBUG 0
 %define CONFIG_REALTIME_ONLY 1
diff --git a/third_party/libaom/source/config/win/ia32/config/aom_config.h b/third_party/libaom/source/config/win/ia32/config/aom_config.h
index d94b501..bc36f4b 100644
--- a/third_party/libaom/source/config/win/ia32/config/aom_config.h
+++ b/third_party/libaom/source/config/win/ia32/config/aom_config.h
@@ -53,6 +53,7 @@
 #define CONFIG_OS_SUPPORT 1
 #define CONFIG_PARTITION_SEARCH_ORDER 0
 #define CONFIG_PIC 1
+#define CONFIG_RATECTRL_LOG 0
 #define CONFIG_RD_COMMAND 0
 #define CONFIG_RD_DEBUG 0
 #define CONFIG_REALTIME_ONLY 1
diff --git a/third_party/libaom/source/config/win/ia32/config/aom_dsp_rtcd.h b/third_party/libaom/source/config/win/ia32/config/aom_dsp_rtcd.h
index ceef0144..9c802bc2 100644
--- a/third_party/libaom/source/config/win/ia32/config/aom_dsp_rtcd.h
+++ b/third_party/libaom/source/config/win/ia32/config/aom_dsp_rtcd.h
@@ -1979,6 +1979,31 @@
 unsigned int aom_get_mb_ss_sse2(const int16_t*);
 #define aom_get_mb_ss aom_get_mb_ss_sse2
 
+void aom_get_sse_sum_8x8_quad_c(const uint8_t* src_ptr,
+                                int source_stride,
+                                const uint8_t* ref_ptr,
+                                int ref_stride,
+                                unsigned int* sse,
+                                int* sum);
+void aom_get_sse_sum_8x8_quad_sse2(const uint8_t* src_ptr,
+                                   int source_stride,
+                                   const uint8_t* ref_ptr,
+                                   int ref_stride,
+                                   unsigned int* sse,
+                                   int* sum);
+void aom_get_sse_sum_8x8_quad_avx2(const uint8_t* src_ptr,
+                                   int source_stride,
+                                   const uint8_t* ref_ptr,
+                                   int ref_stride,
+                                   unsigned int* sse,
+                                   int* sum);
+RTCD_EXTERN void (*aom_get_sse_sum_8x8_quad)(const uint8_t* src_ptr,
+                                             int source_stride,
+                                             const uint8_t* ref_ptr,
+                                             int ref_stride,
+                                             unsigned int* sse,
+                                             int* sum);
+
 void aom_h_predictor_16x16_c(uint8_t* dst,
                              ptrdiff_t y_stride,
                              const uint8_t* above,
@@ -8612,6 +8637,9 @@
   aom_get_blk_sse_sum = aom_get_blk_sse_sum_sse2;
   if (flags & HAS_AVX2)
     aom_get_blk_sse_sum = aom_get_blk_sse_sum_avx2;
+  aom_get_sse_sum_8x8_quad = aom_get_sse_sum_8x8_quad_sse2;
+  if (flags & HAS_AVX2)
+    aom_get_sse_sum_8x8_quad = aom_get_sse_sum_8x8_quad_avx2;
   aom_h_predictor_32x32 = aom_h_predictor_32x32_sse2;
   if (flags & HAS_AVX2)
     aom_h_predictor_32x32 = aom_h_predictor_32x32_avx2;
diff --git a/third_party/libaom/source/config/win/x64/config/aom_config.asm b/third_party/libaom/source/config/win/x64/config/aom_config.asm
index d4892f7..5f04a0f 100644
--- a/third_party/libaom/source/config/win/x64/config/aom_config.asm
+++ b/third_party/libaom/source/config/win/x64/config/aom_config.asm
@@ -41,6 +41,7 @@
 %define CONFIG_OS_SUPPORT 1
 %define CONFIG_PARTITION_SEARCH_ORDER 0
 %define CONFIG_PIC 0
+%define CONFIG_RATECTRL_LOG 0
 %define CONFIG_RD_COMMAND 0
 %define CONFIG_RD_DEBUG 0
 %define CONFIG_REALTIME_ONLY 1
diff --git a/third_party/libaom/source/config/win/x64/config/aom_config.h b/third_party/libaom/source/config/win/x64/config/aom_config.h
index d92960bc..f212127f 100644
--- a/third_party/libaom/source/config/win/x64/config/aom_config.h
+++ b/third_party/libaom/source/config/win/x64/config/aom_config.h
@@ -53,6 +53,7 @@
 #define CONFIG_OS_SUPPORT 1
 #define CONFIG_PARTITION_SEARCH_ORDER 0
 #define CONFIG_PIC 0
+#define CONFIG_RATECTRL_LOG 0
 #define CONFIG_RD_COMMAND 0
 #define CONFIG_RD_DEBUG 0
 #define CONFIG_REALTIME_ONLY 1
diff --git a/third_party/libaom/source/config/win/x64/config/aom_dsp_rtcd.h b/third_party/libaom/source/config/win/x64/config/aom_dsp_rtcd.h
index 94a1db4..eed2759 100644
--- a/third_party/libaom/source/config/win/x64/config/aom_dsp_rtcd.h
+++ b/third_party/libaom/source/config/win/x64/config/aom_dsp_rtcd.h
@@ -1982,6 +1982,31 @@
 unsigned int aom_get_mb_ss_sse2(const int16_t*);
 #define aom_get_mb_ss aom_get_mb_ss_sse2
 
+void aom_get_sse_sum_8x8_quad_c(const uint8_t* src_ptr,
+                                int source_stride,
+                                const uint8_t* ref_ptr,
+                                int ref_stride,
+                                unsigned int* sse,
+                                int* sum);
+void aom_get_sse_sum_8x8_quad_sse2(const uint8_t* src_ptr,
+                                   int source_stride,
+                                   const uint8_t* ref_ptr,
+                                   int ref_stride,
+                                   unsigned int* sse,
+                                   int* sum);
+void aom_get_sse_sum_8x8_quad_avx2(const uint8_t* src_ptr,
+                                   int source_stride,
+                                   const uint8_t* ref_ptr,
+                                   int ref_stride,
+                                   unsigned int* sse,
+                                   int* sum);
+RTCD_EXTERN void (*aom_get_sse_sum_8x8_quad)(const uint8_t* src_ptr,
+                                             int source_stride,
+                                             const uint8_t* ref_ptr,
+                                             int ref_stride,
+                                             unsigned int* sse,
+                                             int* sum);
+
 void aom_h_predictor_16x16_c(uint8_t* dst,
                              ptrdiff_t y_stride,
                              const uint8_t* above,
@@ -8651,6 +8676,9 @@
   aom_get_blk_sse_sum = aom_get_blk_sse_sum_sse2;
   if (flags & HAS_AVX2)
     aom_get_blk_sse_sum = aom_get_blk_sse_sum_avx2;
+  aom_get_sse_sum_8x8_quad = aom_get_sse_sum_8x8_quad_sse2;
+  if (flags & HAS_AVX2)
+    aom_get_sse_sum_8x8_quad = aom_get_sse_sum_8x8_quad_avx2;
   aom_h_predictor_32x32 = aom_h_predictor_32x32_sse2;
   if (flags & HAS_AVX2)
     aom_h_predictor_32x32 = aom_h_predictor_32x32_avx2;
diff --git a/third_party/webgpu-cts/ts_sources.txt b/third_party/webgpu-cts/ts_sources.txt
index 14dc8d6..b22e3c3 100644
--- a/third_party/webgpu-cts/ts_sources.txt
+++ b/third_party/webgpu-cts/ts_sources.txt
@@ -3,10 +3,11 @@
 src/common/internal/logging/log_message.ts
 src/common/internal/logging/result.ts
 src/common/internal/logging/logger.ts
+src/common/util/types.ts
+src/common/util/data_tables.ts
 src/common/util/timeout.ts
 src/common/util/util.ts
 src/common/internal/logging/test_case_recorder.ts
-src/common/util/types.ts
 src/common/runtime/helper/options.ts
 src/common/internal/query/encode_selectively.ts
 src/common/internal/query/json_param_value.ts
@@ -42,7 +43,6 @@
 src/common/tools/version.ts
 src/common/util/collect_garbage.ts
 src/common/util/colors.ts
-src/common/util/data_tables.ts
 src/common/util/preprocessor.ts
 src/unittests/unit_test.ts
 src/demo/a.spec.ts
diff --git a/third_party/xcbproto/README.chromium b/third_party/xcbproto/README.chromium
index 0f97b77..8993074 100644
--- a/third_party/xcbproto/README.chromium
+++ b/third_party/xcbproto/README.chromium
@@ -1,7 +1,7 @@
 Name: xcbproto
 Short Name: xcbproto
 URL: https://gitlab.freedesktop.org/xorg/proto/xcbproto
-Version: 496e3ce329c3cc9b32af4054c30fa0f306deb007
+Version: 70ca65fa35c3760661b090bc4b2601daa7a099b8
 CPEPrefix: unknown
 Security Critical: no
 License: MIT
diff --git a/third_party/xcbproto/VERSION b/third_party/xcbproto/VERSION
index b64ae9b0..626f0e2a 100644
--- a/third_party/xcbproto/VERSION
+++ b/third_party/xcbproto/VERSION
@@ -1 +1 @@
-496e3ce329c3cc9b32af4054c30fa0f306deb007
+70ca65fa35c3760661b090bc4b2601daa7a099b8
diff --git a/third_party/xcbproto/patch.diff b/third_party/xcbproto/patch.diff
index 2800338..1d72d6f 100644
--- a/third_party/xcbproto/patch.diff
+++ b/third_party/xcbproto/patch.diff
@@ -1,6 +1,6 @@
 diff -ru xcbproto/src/glx.xml src/src/glx.xml
---- xcbproto/src/glx.xml	2021-02-10 12:51:13.785903766 -0800
-+++ src/src/glx.xml	2021-02-04 15:13:24.836890047 -0800
+--- xcbproto/src/glx.xml	2022-03-25 16:30:06.066425385 -0700
++++ src/src/glx.xml	2022-03-25 16:29:28.146096207 -0700
 @@ -62,8 +62,6 @@
              <type>glx:WINDOW</type>
          </xidunion>
@@ -292,8 +292,8 @@
  			</list>
  		</reply>
 diff -ru xcbproto/src/present.xml src/src/present.xml
---- xcbproto/src/present.xml	2021-02-10 12:51:13.785903766 -0800
-+++ src/src/present.xml	2021-02-10 12:51:07.933863954 -0800
+--- xcbproto/src/present.xml	2022-03-25 16:30:06.050425246 -0700
++++ src/src/present.xml	2022-03-25 16:27:47.661223477 -0700
 @@ -89,7 +89,7 @@
      </reply>
    </request>
@@ -304,8 +304,8 @@
      <field type="WINDOW" name="window" />
      <field type="PIXMAP" name="pixmap" />
 diff -ru xcbproto/src/randr.xml src/src/randr.xml
---- xcbproto/src/randr.xml	2021-02-10 12:51:13.785903766 -0800
-+++ src/src/randr.xml	2021-02-04 11:51:16.530051106 -0800
+--- xcbproto/src/randr.xml	2022-03-25 16:30:06.050425246 -0700
++++ src/src/randr.xml	2022-03-25 16:27:47.661223477 -0700
 @@ -803,64 +803,6 @@
  		<item name="Lease">           <value>6</value></item>
  	</enum>
@@ -479,8 +479,8 @@
  	</event>
  </xcb>
 diff -ru xcbproto/src/shm.xml src/src/shm.xml
---- xcbproto/src/shm.xml	2021-02-10 12:51:13.789903793 -0800
-+++ src/src/shm.xml	2021-02-04 11:51:16.530051106 -0800
+--- xcbproto/src/shm.xml	2022-03-25 16:30:06.050425246 -0700
++++ src/src/shm.xml	2022-03-25 16:27:47.661223477 -0700
 @@ -78,7 +78,7 @@
      <field type="INT16" name="dst_x" />
      <field type="INT16" name="dst_y" />
@@ -491,8 +491,8 @@
      <pad bytes="1" />
      <field type="SEG" name="shmseg" />
 diff -ru xcbproto/src/xinput.xml src/src/xinput.xml
---- xcbproto/src/xinput.xml	2021-02-10 12:51:13.789903793 -0800
-+++ src/src/xinput.xml	2021-02-08 17:31:46.995137506 -0800
+--- xcbproto/src/xinput.xml	2022-03-25 16:30:06.066425385 -0700
++++ src/src/xinput.xml	2022-03-25 16:29:28.146096207 -0700
 @@ -200,7 +200,12 @@
              <list type="STR" name="names">
                  <fieldref>devices_len</fieldref>
@@ -693,8 +693,8 @@
      <!-- ⋅⋅⋅ Events (v2.3) ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ -->
  
 diff -ru xcbproto/src/xproto.xml src/src/xproto.xml
---- xcbproto/src/xproto.xml	2021-02-10 12:51:13.789903793 -0800
-+++ src/src/xproto.xml	2021-02-04 15:13:24.840890072 -0800
+--- xcbproto/src/xproto.xml	2022-03-25 16:30:06.070425420 -0700
++++ src/src/xproto.xml	2022-03-25 16:29:28.150096242 -0700
 @@ -963,6 +963,7 @@
      <item name="CAP_HEIGHT">           <value>66</value> </item>
      <item name="WM_CLASS">             <value>67</value> </item>
@@ -737,15 +737,10 @@
    </request>
  
    <!-- Reply from SetPointerMapping or SetModifierMapping -->
-Only in src/xcbgen: align.pyc
-Only in src/xcbgen: error.pyc
-Only in src/xcbgen: expr.pyc
-Only in src/xcbgen: __init__.pyc
-Only in src/xcbgen: matcher.pyc
-Only in src/xcbgen: state.pyc
+Only in src/xcbgen: __pycache__
 diff -ru xcbproto/xcbgen/xtypes.py src/xcbgen/xtypes.py
---- xcbproto/xcbgen/xtypes.py	2021-02-10 12:51:13.789903793 -0800
-+++ src/xcbgen/xtypes.py	2021-02-04 15:13:24.840890072 -0800
+--- xcbproto/xcbgen/xtypes.py	2022-03-25 16:30:06.070425420 -0700
++++ src/xcbgen/xtypes.py	2022-03-25 16:29:28.150096242 -0700
 @@ -226,7 +226,7 @@
      Derived class which represents a file descriptor.
      '''
@@ -755,4 +750,3 @@
          self.is_fd = True
  
      def fixed_size(self):
-Only in src/xcbgen: xtypes.pyc
diff --git a/third_party/xcbproto/src/doc/xml-xcb.txt b/third_party/xcbproto/src/doc/xml-xcb.txt
index f5b9aed..baef734 100644
--- a/third_party/xcbproto/src/doc/xml-xcb.txt
+++ b/third_party/xcbproto/src/doc/xml-xcb.txt
@@ -65,8 +65,8 @@
 
   This element represents a data structure.  The name attribute gives the name
   of the structure.  The content represents the fields of the structure, and
-  consists of one or more of the field, pad, and list elements described in
-  the section "Structure Contents" below.
+  consists of one or more of the length, field, pad, and list elements described
+  in the section "Structure Contents" below.
 
 <union name="identifier">structure contents</union>
 
@@ -215,6 +215,23 @@
   declares the data type of the field, and the name attribute gives the name
   of the field.
 
+<length>expression</length>
+  This element overrides the length of the data structure by specifying it
+  explicitly instead of it being defined by the layout of the structure.
+  This makes it possible to handle structures with conditional fields
+  (see the <switch> element) where the future revisions of protocols may
+  introduce new variants and old code must still properly ignore them.
+
+  The content is an expression giving the length of the data structure in terms
+  of other fields in the structure.  See the section "Expressions" for details
+  on the expression representation.
+
+  The expression must not depend on conditional fields.
+
+  Additionally, the length of the data structure must be at least such that it
+  includes the fields that the expression depends on. Smaller length is
+  considered a violation of the protocol.
+
 <fd name="identifier" />
 
   This element represents a file descriptor field passed with the request.  The
diff --git a/third_party/xcbproto/src/src/glx.xml b/third_party/xcbproto/src/src/glx.xml
index 70aa9f9..6e728e2 100644
--- a/third_party/xcbproto/src/src/glx.xml
+++ b/third_party/xcbproto/src/src/glx.xml
@@ -489,6 +489,7 @@
 		<list type="char" name="gl_extension_string">
 			<fieldref>gl_str_len</fieldref>
 		</list>
+		<pad align="4" />
 		<list type="char" name="glx_extension_string">
 			<fieldref>glx_str_len</fieldref>
 		</list>
@@ -525,6 +526,7 @@
 		<list type="char" name="gl_extension_string">
 			<fieldref>gl_str_len</fieldref>
 		</list>
+		<pad align="4" />
 		<list type="char" name="glx_extension_string">
 			<fieldref>glx_str_len</fieldref>
 		</list>
diff --git a/third_party/xcbproto/src/src/xcb.xsd b/third_party/xcbproto/src/src/xcb.xsd
index dc3d7cc..86a51c5 100644
--- a/third_party/xcbproto/src/src/xcb.xsd
+++ b/third_party/xcbproto/src/src/xcb.xsd
@@ -101,6 +101,13 @@
   <!-- field replaces FIELD, PARAM, and REPLY. -->
   <xsd:element name="field" type="var" />
 
+  <!-- Length of data structures -->
+  <xsd:element name="length">
+    <xsd:complexType>
+      <xsd:group ref="expression" />
+    </xsd:complexType>
+  </xsd:element>
+
   <!-- fd passing parameter -->
   <xsd:element name="fd">
     <xsd:complexType>
@@ -210,6 +217,7 @@
       <xsd:element ref="list" />
       <xsd:element ref="fd" />
       <xsd:element ref="required_start_align" />
+      <xsd:element ref="length" />
     </xsd:choice>
   </xsd:group>
 
diff --git a/third_party/xcbproto/src/src/xfixes.xml b/third_party/xcbproto/src/src/xfixes.xml
index 0a3d5ff..a01cd7b0 100644
--- a/third_party/xcbproto/src/src/xfixes.xml
+++ b/third_party/xcbproto/src/src/xfixes.xml
@@ -26,7 +26,7 @@
 -->
 <!-- This file describes version 4 of XFixes. -->
 <xcb header="xfixes" extension-xname="XFIXES" extension-name="XFixes"
-    major-version="5" minor-version="0">
+    major-version="6" minor-version="0">
   <import>xproto</import>
   <import>render</import>
   <import>shape</import>
@@ -359,4 +359,24 @@
   <request name="DeletePointerBarrier" opcode="32">
     <field type="BARRIER" name="barrier" />
   </request>
+
+  <!-- Version 6 -->
+
+  <enum name="ClientDisconnectFlags">
+    <item name="Default"><value>0</value></item>
+    <item name="Terminate"><bit>0</bit></item>
+  </enum>
+
+  <request name="SetClientDisconnectMode" opcode="33">
+    <field type="CARD32" name="disconnect_mode" mask="ClientDisconnectFlags" />
+  </request>
+
+  <request name="GetClientDisconnectMode" opcode="34">
+    <reply>
+      <pad bytes="1" />
+      <field type="CARD32" name="disconnect_mode" mask="ClientDisconnectFlags" />
+      <pad bytes="20" />
+    </reply>
+  </request>
+
 </xcb>
diff --git a/third_party/xcbproto/src/src/xinput.xml b/third_party/xcbproto/src/src/xinput.xml
index 33a93d2..90db8f9 100644
--- a/third_party/xcbproto/src/src/xinput.xml
+++ b/third_party/xcbproto/src/src/xinput.xml
@@ -33,7 +33,7 @@
 -->
 
 <xcb header="xinput" extension-xname="XInputExtension" extension-name="Input"
-     major-version="2" minor-version="3">
+     major-version="2" minor-version="4">
     <import>xfixes</import>
     <import>xproto</import>
 
@@ -1607,6 +1607,7 @@
         <item name="Valuator"> <value>2</value> </item>
         <item name="Scroll">   <value>3</value> </item>
         <item name="Touch">    <value>8</value> </item>
+        <item name="Gesture">  <value>9</value> </item>
     </enum>
 
     <enum name="DeviceType">
@@ -1680,6 +1681,14 @@
         <field type="CARD8"    name="num_touches" />
     </struct>
 
+    <struct name="GestureClass">
+        <field type="CARD16"   name="type" enum="DeviceClassType" />
+        <field type="CARD16"   name="len" />
+        <field type="DeviceId" name="sourceid" />
+        <field type="CARD8"    name="num_touches" />
+        <pad bytes="1" />
+    </struct>
+
     <struct name="ValuatorClass">
         <field type="CARD16"   name="type" enum="DeviceClassType" />
         <field type="CARD16"   name="len" />
@@ -1695,6 +1704,12 @@
     </struct>
 
     <struct name="DeviceClass">
+        <length>
+            <op op="*">
+                <fieldref>len</fieldref>
+                <value>4</value>
+            </op>
+        </length>
         <field type="CARD16"   name="type" enum="DeviceClassType" />
         <field type="CARD16"   name="len" />
         <field type="DeviceId" name="sourceid" />
@@ -1752,6 +1767,11 @@
 		<field type="CARD8"    name="mode" enum="TouchMode" />
 		<field type="CARD8"    name="num_touches" />
 	    </case>
+        <case name="gesture">
+            <enumref ref="DeviceClassType">Gesture</enumref>
+            <field type="CARD8"    name="num_touches" />
+            <pad bytes="1" />
+        </case>
 	</switch>
     </struct>
 
@@ -1872,11 +1892,13 @@
     </enum>
 
     <enum name="GrabType">
-        <item name="Button">     <value>0</value> </item>
-        <item name="Keycode">    <value>1</value> </item>
-        <item name="Enter">      <value>2</value> </item>
-        <item name="FocusIn">    <value>3</value> </item>
-        <item name="TouchBegin"> <value>4</value> </item>
+        <item name="Button">            <value>0</value> </item>
+        <item name="Keycode">           <value>1</value> </item>
+        <item name="Enter">             <value>2</value> </item>
+        <item name="FocusIn">           <value>3</value> </item>
+        <item name="TouchBegin">        <value>4</value> </item>
+        <item name="GesturePinchBegin"> <value>5</value> </item>
+        <item name="GestureSwipeBegin"> <value>6</value> </item>
     </enum>
 
     <enum name="ModifierMask">
@@ -2485,6 +2507,72 @@
 
     <eventcopy name="BarrierLeave" number="26" ref="BarrierHit" />
 
+    <!-- ⋅⋅⋅ Events (v2.4) ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ -->
+
+    <enum name="GesturePinchEventFlags">
+        <item name="GesturePinchCancelled"> <bit>0</bit> </item>
+    </enum>
+
+    <event name="GesturePinchBegin" number="27" xge="true">
+        <field type="DeviceId"  name="deviceid" altenum="Device" />
+        <field type="TIMESTAMP" name="time" altenum="Time" />
+        <!-- event specific fields -->
+        <field type="CARD32"    name="detail" />
+        <field type="WINDOW"    name="root" />
+        <field type="WINDOW"    name="event" />
+        <field type="WINDOW"    name="child" />
+        <!-- 32 byte boundary -->
+        <field type="FP1616"    name="root_x" />
+        <field type="FP1616"    name="root_y" />
+        <field type="FP1616"    name="event_x" />
+        <field type="FP1616"    name="event_y" />
+        <field type="FP1616"    name="delta_x" />
+        <field type="FP1616"    name="delta_y" />
+        <field type="FP1616"    name="delta_unaccel_x" />
+        <field type="FP1616"    name="delta_unaccel_y" />
+        <field type="FP1616"    name="scale" />
+        <field type="FP1616"    name="delta_angle" />
+        <field type="DeviceId"  name="sourceid" altenum="Device" />
+        <pad bytes="2" />
+        <field type="ModifierInfo" name="mods" />
+        <field type="GroupInfo"    name="group" />
+        <field type="CARD32"       name="flags" mask="GesturePinchEventFlags" />
+    </event>
+
+    <eventcopy name="GesturePinchUpdate" number="28" ref="GesturePinchBegin" />
+    <eventcopy name="GesturePinchEnd"    number="29" ref="GesturePinchBegin" />
+
+    <enum name="GestureSwipeEventFlags">
+        <item name="GestureSwipeCancelled"> <bit>0</bit> </item>
+    </enum>
+
+    <event name="GestureSwipeBegin" number="30" xge="true">
+        <field type="DeviceId"  name="deviceid" altenum="Device" />
+        <field type="TIMESTAMP" name="time" altenum="Time" />
+        <!-- event specific fields -->
+        <field type="CARD32"    name="detail" />
+        <field type="WINDOW"    name="root" />
+        <field type="WINDOW"    name="event" />
+        <field type="WINDOW"    name="child" />
+        <!-- 32 byte boundary -->
+        <field type="FP1616"    name="root_x" />
+        <field type="FP1616"    name="root_y" />
+        <field type="FP1616"    name="event_x" />
+        <field type="FP1616"    name="event_y" />
+        <field type="FP1616"    name="delta_x" />
+        <field type="FP1616"    name="delta_y" />
+        <field type="FP1616"    name="delta_unaccel_x" />
+        <field type="FP1616"    name="delta_unaccel_y" />
+        <field type="DeviceId"  name="sourceid" altenum="Device" />
+        <pad bytes="2" />
+        <field type="ModifierInfo" name="mods" />
+        <field type="GroupInfo"    name="group" />
+        <field type="CARD32"       name="flags" mask="GestureSwipeEventFlags" />
+    </event>
+
+    <eventcopy name="GestureSwipeUpdate" number="31" ref="GestureSwipeBegin" />
+    <eventcopy name="GestureSwipeEnd"    number="32" ref="GestureSwipeBegin" />
+
     <!-- ⋅⋅⋅ Requests that depend on events ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ -->
 
     <!-- SendExtensionEvent -->
diff --git a/third_party/xcbproto/src/src/xprint.xml b/third_party/xcbproto/src/src/xprint.xml
index f9af65fa..fa3bb7f 100644
--- a/third_party/xcbproto/src/src/xprint.xml
+++ b/third_party/xcbproto/src/src/xprint.xml
@@ -102,7 +102,7 @@
         <list type="STRING8" name="printer_name">
             <fieldref>printerNameLen</fieldref>
         </list>
-        <!-- There's some padding in here... -->
+        <pad align="4" />
         <list type="STRING8" name="locale">
             <fieldref>localeLen</fieldref>
         </list>
@@ -125,7 +125,7 @@
         <list type="STRING8" name="printerName">
             <fieldref>printerNameLen</fieldref>
         </list>
-        <!-- padding -->
+        <pad align="4" />
         <list type="STRING8" name="locale">
             <fieldref>localeLen</fieldref>
         </list>
@@ -177,11 +177,11 @@
         <list type="BYTE" name="data">
             <fieldref>len_data</fieldref>
         </list>
-        <!-- padding -->
+        <pad align="4" />
         <list type="STRING8" name="doc_format">
             <fieldref>len_fmt</fieldref>
         </list>
-        <!-- padding -->
+        <pad align="4" />
         <list type="STRING8" name="options">
             <fieldref>len_options</fieldref>
         </list>
diff --git a/third_party/xcbproto/src/src/xproto.xml b/third_party/xcbproto/src/src/xproto.xml
index 928a1c39..21f332f 100644
--- a/third_party/xcbproto/src/src/xproto.xml
+++ b/third_party/xcbproto/src/src/xproto.xml
@@ -2982,25 +2982,21 @@
 
       ]]></description>
       <field name="owner_events"><![CDATA[
-If 1, the `grab_window` will still get the pointer events. If 0, events are not
+If 1, the `grab_window` will still get the key events. If 0, events are not
 reported to the `grab_window`.
       ]]></field>
       <field name="grab_window"><![CDATA[
-Specifies the window on which the pointer should be grabbed.
+Specifies the window on which the key should be grabbed.
       ]]></field>
       <field name="key"><![CDATA[
 The keycode of the key to grab.
 
 The special value `XCB_GRAB_ANY` means grab any key.
       ]]></field>
-      <field name="cursor"><![CDATA[
-Specifies the cursor that should be displayed or `XCB_NONE` to not change the
-cursor.
-      ]]></field>
       <field name="modifiers"><![CDATA[
 The modifiers to grab.
 
-Using the special value `XCB_MOD_MASK_ANY` means grab the pointer with all
+Using the special value `XCB_MOD_MASK_ANY` means grab the key with all
 possible modifier combinations.
       ]]></field>
       <!-- the enum doc is sufficient. -->
@@ -3011,7 +3007,8 @@
 combination on the same window.
       ]]></error>
       <error type="Value"><![CDATA[
-TODO: reasons?
+The key is not `XCB_GRAB_ANY` and not in the range specified by `min_keycode`
+and `max_keycode` in the connection setup.
       ]]></error>
       <error type="Window"><![CDATA[
 The specified `window` does not exist.
diff --git a/third_party/xcbproto/src/xcb-proto.pc.in b/third_party/xcbproto/src/xcb-proto.pc.in
index a35f0bd..c7c8b47 100644
--- a/third_party/xcbproto/src/xcb-proto.pc.in
+++ b/third_party/xcbproto/src/xcb-proto.pc.in
@@ -4,6 +4,7 @@
 datadir=@datadir@
 libdir=@libdir@
 xcbincludedir=${pc_sysrootdir}@xcbincludedir@
+PYTHON_PREFIX=@PYTHON_PREFIX@
 pythondir=${pc_sysrootdir}@pythondir@
 
 Name: XCB Proto
diff --git a/third_party/xcbproto/src/xcbgen/xtypes.py b/third_party/xcbproto/src/xcbgen/xtypes.py
index 0cabf2d7..3603233 100644
--- a/third_party/xcbproto/src/xcbgen/xtypes.py
+++ b/third_party/xcbproto/src/xcbgen/xtypes.py
@@ -1,8 +1,15 @@
 '''
 This module contains the classes which represent XCB data types.
 '''
+import sys
 from xcbgen.expr import Field, Expression
 from xcbgen.align import Alignment, AlignmentLog
+
+if sys.version_info[:2] >= (3, 3):
+    from xml.etree.ElementTree import SubElement
+else:
+    from xml.etree.cElementTree import SubElement
+
 import __main__
 
 verbose_align_log = False
@@ -503,6 +510,8 @@
 
     Public fields added:
     fields is an array of Field objects describing the structure fields.
+    length_expr is an expression that defines the length of the structure.
+
     '''
     def __init__(self, name, elt):
         Type.__init__(self, name)
@@ -512,6 +521,7 @@
         self.nmemb = 1
         self.size = 0
         self.lenfield_parent = [self]
+        self.length_expr = None
 
         # get required_start_alignment
         required_start_align_element = elt.find("required_start_align")
@@ -573,6 +583,9 @@
                 type = module.get_type('INT32')
                 type.make_fd_of(module, self, fd_name)
                 continue
+            elif child.tag == 'length':
+                self.length_expr = Expression(list(child)[0], self)
+                continue
             else:
                 # Hit this on Reply
                 continue
@@ -1346,6 +1359,15 @@
         if self.required_start_align is None:
             self.required_start_align = Alignment(4,0)
 
+        # All errors are basically the same, but they still got different XML
+        # for historic reasons. This 'invents' the missing parts.
+        if len(self.elt) < 1:
+            SubElement(self.elt, "field", type="CARD32", name="bad_value")
+        if len(self.elt) < 2:
+            SubElement(self.elt, "field", type="CARD16", name="minor_opcode")
+        if len(self.elt) < 3:
+            SubElement(self.elt, "field", type="CARD8", name="major_opcode")
+
     def add_opcode(self, opcode, name, main):
         self.opcodes[name] = opcode
         if main:
diff --git a/tools/binary_size/libsupersize/viewer/static/main.css b/tools/binary_size/libsupersize/viewer/static/main.css
index 5d1dfe3..1956214e 100644
--- a/tools/binary_size/libsupersize/viewer/static/main.css
+++ b/tools/binary_size/libsupersize/viewer/static/main.css
@@ -291,4 +291,10 @@
   color: #0000d1;
   text-decoration: none;
   white-space: pre;
+}
+
+#no-symbols-msg {
+  position: relative;
+  text-align: center;
+  top: -1em;
 }
\ No newline at end of file
diff --git a/tools/binary_size/libsupersize/viewer/static/tree-ui.js b/tools/binary_size/libsupersize/viewer/static/tree-ui.js
index 33dee93..de56ccb 100644
--- a/tools/binary_size/libsupersize/viewer/static/tree-ui.js
+++ b/tools/binary_size/libsupersize/viewer/static/tree-ui.js
@@ -555,8 +555,7 @@
    * @param {boolean} show
    */
   function toggleNoSymbolsMessage(show) {
-    const errorModal = document.getElementById('error-modal');
-    errorModal.querySelector('div').style.alignItems = 'center';
+    const errorModal = document.getElementById('no-symbols-msg');
     errorModal.style.display = show ? '' : 'none';
   }
 
diff --git a/tools/binary_size/libsupersize/viewer/static/viewer.html b/tools/binary_size/libsupersize/viewer/static/viewer.html
index bd65aff..60c324d 100644
--- a/tools/binary_size/libsupersize/viewer/static/viewer.html
+++ b/tools/binary_size/libsupersize/viewer/static/viewer.html
@@ -410,6 +410,10 @@
       </header>
       <ul id="symboltree" class="tree" role="tree" aria-labelledby="headline"></ul>
     </main>
+    <!-- Empty .sizediff Message-->
+    <div id="no-symbols-msg" style="display:none">
+      This diff contains no symbols (no sizes changed).
+    </div>
     <!-- Metadata Node -->
     <div id="metadata-view" class="metadata" title="Metadata from .size/.sizediff file">
       <a class="node" tabindex="0" role="presentation">
@@ -630,12 +634,6 @@
       <button type="button" class="signin text-button filled-button">Sign Me In</button>
     </div>
   </div>
-  <!-- Modal Empty .sizediff Dialog -->
-  <div id="error-modal" class="modal" style="display:none">
-    <div class="modal-content">
-      <p>This diff contains no symbols (no sizes changed).</p>
-    </div>
-  </div>
 </body>
 
 </html>
diff --git a/tools/mb/mb.py b/tools/mb/mb.py
index 65b4562..23bc524 100755
--- a/tools/mb/mb.py
+++ b/tools/mb/mb.py
@@ -755,8 +755,9 @@
       if ret != 0:
         return ret
       collect_json = json.loads(self.ReadFile(collect_output))
-      # The exit_code field is not included if the task was successful.
-      ret = collect_json.get(task_id, {}).get('results', {}).get('exit_code', 0)
+      # The exit_code field might not be included if the task was successful.
+      ret = int(
+          collect_json.get(task_id, {}).get('results', {}).get('exit_code', 0))
     finally:
       if json_dir:
         self.RemoveDirectory(json_dir)
diff --git a/tools/mb/mb_config.pyl b/tools/mb/mb_config.pyl
index eee0fbb2..0572b8a1 100644
--- a/tools/mb/mb_config.pyl
+++ b/tools/mb/mb_config.pyl
@@ -486,7 +486,7 @@
       'GPU Linux Builder (dbg)': 'gpu_tests_debug_bot_reclient',
       'GPU Win x64 Builder': 'gpu_tests_release_bot_dcheck_always_on_resource_allowlisting',
       'GPU Win x64 Builder Code Coverage': 'gpu_tests_release_trybot_resource_allowlisting_code_coverage',
-      'GPU Win x64 Builder (dbg)': 'gpu_tests_debug_bot',
+      'GPU Win x64 Builder (dbg)': 'gpu_tests_debug_bot_reclient',
       'Android Release (Nexus 5X)': 'gpu_tests_android_release_bot_dcheck_always_on_arm64_fastbuild_reclient',
     },
 
@@ -508,15 +508,15 @@
       'GPU FYI Mac Builder (dbg)': 'gpu_fyi_tests_debug_trybot',
       'GPU FYI Mac arm64 Builder': 'gpu_fyi_tests_release_trybot_arm64',
       'GPU FYI Win Builder': 'gpu_fyi_tests_release_trybot_x86',
-      'GPU FYI Win x64 Builder': 'gpu_fyi_tests_release_trybot',
+      'GPU FYI Win x64 Builder': 'gpu_fyi_tests_release_trybot_reclient',
       'GPU FYI Win x64 Builder (reclient shadow)': 'gpu_fyi_tests_release_trybot_reclient',
-      'GPU FYI Win x64 Builder (dbg)': 'gpu_fyi_tests_debug_trybot',
+      'GPU FYI Win x64 Builder (dbg)': 'gpu_fyi_tests_debug_trybot_reclient',
       'GPU FYI Win x64 Builder (dbg) (reclient shadow)': 'gpu_fyi_tests_debug_trybot_reclient',
-      'GPU FYI Win x64 DX12 Vulkan Builder': 'gpu_fyi_tests_dx12vk_release_trybot',
+      'GPU FYI Win x64 DX12 Vulkan Builder': 'gpu_fyi_tests_dx12vk_release_trybot_reclient',
       'GPU FYI Win x64 DX12 Vulkan Builder (reclient shadow)': 'gpu_fyi_tests_dx12vk_release_trybot_reclient',
-      'GPU FYI Win x64 DX12 Vulkan Builder (dbg)': 'gpu_fyi_tests_dx12vk_debug_trybot',
+      'GPU FYI Win x64 DX12 Vulkan Builder (dbg)': 'gpu_fyi_tests_dx12vk_debug_trybot_reclient',
       'GPU FYI Win x64 DX12 Vulkan Builder (dbg) (reclient shadow)': 'gpu_fyi_tests_dx12vk_debug_trybot_reclient',
-      'GPU FYI XR Win x64 Builder': 'gpu_fyi_tests_release_trybot',
+      'GPU FYI XR Win x64 Builder': 'gpu_fyi_tests_release_trybot_reclient',
       'GPU FYI XR Win x64 Builder (reclient shadow)': 'gpu_fyi_tests_release_trybot_reclient',
       'GPU Win x64 Builder (dbg) (reclient shadow)': 'gpu_tests_debug_bot_reclient',
       'Linux FYI GPU TSAN Release': 'gpu_fyi_tests_release_trybot_tsan_reclient',
diff --git a/tools/mb/mb_config_expectations/chromium.gpu.fyi.json b/tools/mb/mb_config_expectations/chromium.gpu.fyi.json
index bf3779b..979c9acf 100644
--- a/tools/mb/mb_config_expectations/chromium.gpu.fyi.json
+++ b/tools/mb/mb_config_expectations/chromium.gpu.fyi.json
@@ -209,7 +209,8 @@
       "is_gpu_fyi_bot": true,
       "proprietary_codecs": true,
       "symbol_level": 1,
-      "use_goma": true
+      "use_rbe": true,
+      "use_remoteexec": true
     }
   },
   "GPU FYI Win x64 Builder (dbg)": {
@@ -222,7 +223,8 @@
       "is_gpu_fyi_bot": true,
       "proprietary_codecs": true,
       "symbol_level": 1,
-      "use_goma": true
+      "use_rbe": true,
+      "use_remoteexec": true
     }
   },
   "GPU FYI Win x64 Builder (dbg) (reclient shadow)": {
@@ -268,7 +270,8 @@
       "is_gpu_fyi_bot": true,
       "proprietary_codecs": true,
       "symbol_level": 1,
-      "use_goma": true
+      "use_rbe": true,
+      "use_remoteexec": true
     }
   },
   "GPU FYI Win x64 DX12 Vulkan Builder (dbg)": {
@@ -282,7 +285,8 @@
       "is_gpu_fyi_bot": true,
       "proprietary_codecs": true,
       "symbol_level": 1,
-      "use_goma": true
+      "use_rbe": true,
+      "use_remoteexec": true
     }
   },
   "GPU FYI Win x64 DX12 Vulkan Builder (dbg) (reclient shadow)": {
@@ -329,7 +333,8 @@
       "is_gpu_fyi_bot": true,
       "proprietary_codecs": true,
       "symbol_level": 1,
-      "use_goma": true
+      "use_rbe": true,
+      "use_remoteexec": true
     }
   },
   "GPU FYI XR Win x64 Builder (reclient shadow)": {
diff --git a/tools/mb/mb_config_expectations/chromium.gpu.json b/tools/mb/mb_config_expectations/chromium.gpu.json
index 4a18dc2a..101b845 100644
--- a/tools/mb/mb_config_expectations/chromium.gpu.json
+++ b/tools/mb/mb_config_expectations/chromium.gpu.json
@@ -83,7 +83,8 @@
       "is_debug": true,
       "proprietary_codecs": true,
       "symbol_level": 1,
-      "use_goma": true
+      "use_rbe": true,
+      "use_remoteexec": true
     }
   },
   "GPU Win x64 Builder Code Coverage": {
diff --git a/tools/metrics/histograms/enums.xml b/tools/metrics/histograms/enums.xml
index d061bcb..e8590466 100644
--- a/tools/metrics/histograms/enums.xml
+++ b/tools/metrics/histograms/enums.xml
@@ -8640,6 +8640,7 @@
       label="RFH_BEFOREUNLOAD_HANDLER_NOT_ALLOWED_IN_FENCED_FRAME"/>
   <int value="271" label="MSDH_GET_OPEN_DEVICE_USE_WITHOUT_FEATURE"/>
   <int value="272" label="RFHI_SUBFRAME_NAV_WOULD_CHANGE_MAINFRAME_ORIGIN"/>
+  <int value="273" label="FF_CREATE_WHILE_PRERENDERING"/>
 </enum>
 
 <enum name="BadMessageReasonExtensions">
@@ -53261,6 +53262,7 @@
   <int value="-1101334831" label="SyncSupportTrustedVaultPassphrase:disabled"/>
   <int value="-1101036307" label="SetTimeoutWithoutClamp:enabled"/>
   <int value="-1099618411" label="UpdatedCellularActivationUi:enabled"/>
+  <int value="-1099496822" label="CaptureModeSelfieCamera:enabled"/>
   <int value="-1099142083" label="V8Ignition:disabled"/>
   <int value="-1099135056" label="AsyncDns:enabled"/>
   <int value="-1097977406"
@@ -53876,6 +53878,7 @@
   <int value="-681434111" label="WebFeed:disabled"/>
   <int value="-680787130" label="ExperimentalVRFeatures:disabled"/>
   <int value="-680589442" label="MacRTL:disabled"/>
+  <int value="-679585025" label="CaptureModeSelfieCamera:disabled"/>
   <int value="-679500267" label="UseXpsForPrinting:disabled"/>
   <int value="-678184617" label="TranslateSubFrames:disabled"/>
   <int value="-677978627" label="ZeroCopyTabCapture:enabled"/>
diff --git a/tools/metrics/histograms/metadata/ash/histograms.xml b/tools/metrics/histograms/metadata/ash/histograms.xml
index 098348b5..f68e0a7 100644
--- a/tools/metrics/histograms/metadata/ash/histograms.xml
+++ b/tools/metrics/histograms/metadata/ash/histograms.xml
@@ -1641,6 +1641,21 @@
   </summary>
 </histogram>
 
+<histogram
+    name="Ash.DeviceActiveClient.Recorded{DeviceActivityClientTransitionMethod}Minute"
+    units="int" expires_after="2022-11-01">
+  <owner>hirthanan@google.com</owner>
+  <owner>chromeos-data-team@google.com</owner>
+  <summary>
+    Emitted in the minute during the hour that DeviceActivityClient
+    {DeviceActivityClientTransitionMethod} is called. ChromeOS only.
+  </summary>
+  <token key="DeviceActivityClientTransitionMethod">
+    <variant name="TransitionOutOfIdle" summary="transition-out-of-idle"/>
+    <variant name="TransitionToCheckIn" summary="transition-to-check-in"/>
+  </token>
+</histogram>
+
 <histogram name="Ash.DeviceActiveClient.Response.{DeviceActiveClientState}"
     enum="DeviceActiveClientPsmResponse" expires_after="2022-11-01">
   <owner>hirthanan@google.com</owner>
@@ -1664,21 +1679,6 @@
   </summary>
 </histogram>
 
-<histogram
-    name="Ash.DeviceActiveClient.{DeviceActivityClientTransitionMethod}Minute"
-    units="minutes" expires_after="2022-11-01">
-  <owner>hirthanan@google.com</owner>
-  <owner>chromeos-data-team@google.com</owner>
-  <summary>
-    Emitted in the minute during the hour that DeviceActivityClient
-    {DeviceActivityClientTransitionMethod} is called. ChromeOS only.
-  </summary>
-  <token key="DeviceActivityClientTransitionMethod">
-    <variant name="TransitionOutOfIdle" summary="transition-out-of-idle"/>
-    <variant name="TransitionToCheckIn" summary="transition-to-check-in"/>
-  </token>
-</histogram>
-
 <histogram name="Ash.DeviceActiveController.PsmDeviceActiveSecretIsSet"
     enum="BooleanSuccess" expires_after="2022-11-01">
   <owner>hirthanan@google.com</owner>
diff --git a/tools/metrics/histograms/metadata/chromeos_settings/histograms.xml b/tools/metrics/histograms/metadata/chromeos_settings/histograms.xml
index f9c8b58..6963ee4 100644
--- a/tools/metrics/histograms/metadata/chromeos_settings/histograms.xml
+++ b/tools/metrics/histograms/metadata/chromeos_settings/histograms.xml
@@ -75,7 +75,7 @@
 </histogram>
 
 <histogram name="ChromeOS.Settings.Device.KeyboardFunctionKeys"
-    enum="BooleanToggled" expires_after="2022-03-15">
+    enum="BooleanToggled" expires_after="2023-03-15">
   <owner>jimmyxgong@chromium.org</owner>
   <owner>zentaro@chromium.org</owner>
   <owner>cros-peripherals@google.com</owner>
diff --git a/tools/perf/chrome-health-presets.yaml b/tools/perf/chrome-health-presets.yaml
index f91bdd3..a53317b 100644
--- a/tools/perf/chrome-health-presets.yaml
+++ b/tools/perf/chrome-health-presets.yaml
@@ -39,6 +39,10 @@
           - motionmark_ramp_design
           - motionmark_ramp_design
           - motionmark_ramp_design
+          - motionmark_ramp_focus
+          - motionmark_ramp_focus
+          - motionmark_ramp_focus
+          - motionmark_ramp_focus
           - motionmark_ramp_images
           - motionmark_ramp_images
           - motionmark_ramp_images
@@ -76,6 +80,10 @@
           - motionmark_ramp_design
           - motionmark_ramp_design
           - motionmark_ramp_design
+          - motionmark_ramp_focus
+          - motionmark_ramp_focus
+          - motionmark_ramp_focus
+          - motionmark_ramp_focus
           - motionmark_ramp_images
           - motionmark_ramp_images
           - motionmark_ramp_images
@@ -132,6 +140,10 @@
           - motionmark_ramp_design
           - motionmark_ramp_design
           - motionmark_ramp_design
+          - motionmark_ramp_focus
+          - motionmark_ramp_focus
+          - motionmark_ramp_focus
+          - motionmark_ramp_focus
           - motionmark_ramp_images
           - motionmark_ramp_images
           - motionmark_ramp_images
@@ -169,6 +181,10 @@
           - motionmark_ramp_design
           - motionmark_ramp_design
           - motionmark_ramp_design
+          - motionmark_ramp_focus
+          - motionmark_ramp_focus
+          - motionmark_ramp_focus
+          - motionmark_ramp_focus
           - motionmark_ramp_images
           - motionmark_ramp_images
           - motionmark_ramp_images
diff --git a/tools/perf/core/perfetto_binary_roller/binary_deps.json b/tools/perf/core/perfetto_binary_roller/binary_deps.json
index 83d7b63..ae54f585 100644
--- a/tools/perf/core/perfetto_binary_roller/binary_deps.json
+++ b/tools/perf/core/perfetto_binary_roller/binary_deps.json
@@ -13,8 +13,8 @@
             "remote_path": "perfetto_binaries/trace_processor_shell/linux_arm/49b4b5dcbc312d8d2c3751cf29238b8efeb4e494/trace_processor_shell"
         },
         "mac": {
-            "hash": "32d9d37b055e4ac243193f6492b513fcfa386a31",
-            "remote_path": "perfetto_binaries/trace_processor_shell/mac/32214834f51712d2a9b80cd643aee86374270a73/trace_processor_shell"
+            "hash": "0b811b930b30cc65dfc06b66986db179b65e0e63",
+            "remote_path": "perfetto_binaries/trace_processor_shell/mac/3a5b2ad60d85cf76cc94677544bc08c6a0f42ac6/trace_processor_shell"
         },
         "mac_arm64": {
             "hash": "c0397e87456ad6c6a7aa0133e5b81c97adbab4ab",
diff --git a/ui/gfx/geometry/matrix44.cc b/ui/gfx/geometry/matrix44.cc
index 0c1e58b..000ce8f6 100644
--- a/ui/gfx/geometry/matrix44.cc
+++ b/ui/gfx/geometry/matrix44.cc
@@ -165,74 +165,6 @@
   this->setTypeMask(kIdentity_Mask);
 }
 
-void Matrix44::set3x3(SkScalar m_00,
-                      SkScalar m_10,
-                      SkScalar m_20,
-                      SkScalar m_01,
-                      SkScalar m_11,
-                      SkScalar m_21,
-                      SkScalar m_02,
-                      SkScalar m_12,
-                      SkScalar m_22) {
-  fMat[0][0] = m_00;
-  fMat[0][1] = m_10;
-  fMat[0][2] = m_20;
-  fMat[0][3] = 0;
-  fMat[1][0] = m_01;
-  fMat[1][1] = m_11;
-  fMat[1][2] = m_21;
-  fMat[1][3] = 0;
-  fMat[2][0] = m_02;
-  fMat[2][1] = m_12;
-  fMat[2][2] = m_22;
-  fMat[2][3] = 0;
-  fMat[3][0] = 0;
-  fMat[3][1] = 0;
-  fMat[3][2] = 0;
-  fMat[3][3] = 1;
-  this->recomputeTypeMask();
-}
-
-void Matrix44::set3x3RowMajorf(const float src[]) {
-  fMat[0][0] = src[0];
-  fMat[0][1] = src[3];
-  fMat[0][2] = src[6];
-  fMat[0][3] = 0;
-  fMat[1][0] = src[1];
-  fMat[1][1] = src[4];
-  fMat[1][2] = src[7];
-  fMat[1][3] = 0;
-  fMat[2][0] = src[2];
-  fMat[2][1] = src[5];
-  fMat[2][2] = src[8];
-  fMat[2][3] = 0;
-  fMat[3][0] = 0;
-  fMat[3][1] = 0;
-  fMat[3][2] = 0;
-  fMat[3][3] = 1;
-  this->recomputeTypeMask();
-}
-
-void Matrix44::set3x4RowMajorf(const float src[]) {
-  fMat[0][0] = src[0];
-  fMat[1][0] = src[1];
-  fMat[2][0] = src[2];
-  fMat[3][0] = src[3];
-  fMat[0][1] = src[4];
-  fMat[1][1] = src[5];
-  fMat[2][1] = src[6];
-  fMat[3][1] = src[7];
-  fMat[0][2] = src[8];
-  fMat[1][2] = src[9];
-  fMat[2][2] = src[10];
-  fMat[3][2] = src[11];
-  fMat[0][3] = 0;
-  fMat[1][3] = 0;
-  fMat[2][3] = 0;
-  fMat[3][3] = 1;
-  this->recomputeTypeMask();
-}
-
 ///////////////////////////////////////////////////////////////////////////////
 
 Matrix44& Matrix44::setTranslate(SkScalar dx, SkScalar dy, SkScalar dz) {
@@ -331,31 +263,14 @@
 
 ///////////////////////////////////////////////////////////////////////////////
 
-void Matrix44::setRotateAbout(SkScalar x,
-                              SkScalar y,
-                              SkScalar z,
-                              SkScalar radians) {
-  double len2 = static_cast<double>(x) * x + static_cast<double>(y) * y +
-                static_cast<double>(z) * z;
-  if (1 != len2) {
-    if (0 == len2) {
-      this->setIdentity();
-      return;
-    }
-    double scale = 1 / sqrt(len2);
-    x = SkScalar(x * scale);
-    y = SkScalar(y * scale);
-    z = SkScalar(z * scale);
-  }
-  this->setRotateAboutUnit(x, y, z, radians);
-}
-
-void Matrix44::setRotateAboutUnit(SkScalar x,
-                                  SkScalar y,
-                                  SkScalar z,
-                                  SkScalar radians) {
-  double c = cos(radians);
-  double s = sin(radians);
+void Matrix44::setRotateUnitSinCos(SkScalar x,
+                                   SkScalar y,
+                                   SkScalar z,
+                                   SkScalar sin_angle,
+                                   SkScalar cos_angle) {
+  // Use double precision for intermediate results.
+  double c = cos_angle;
+  double s = sin_angle;
   double C = 1 - c;
   double xs = x * s;
   double ys = y * s;
@@ -367,18 +282,90 @@
   double yzC = y * zC;
   double zxC = z * xC;
 
-  // if you're looking at wikipedia, remember that we're column major.
-  this->set3x3(SkScalar(x * xC + c),  // scale x
-               SkScalar(xyC + zs),    // skew x
-               SkScalar(zxC - ys),    // trans x
+  fMat[0][0] = SkDoubleToScalar(x * xC + c);
+  fMat[0][1] = SkDoubleToScalar(xyC + zs);
+  fMat[0][2] = SkDoubleToScalar(zxC - ys);
+  fMat[0][3] = SkDoubleToScalar(0);
+  fMat[1][0] = SkDoubleToScalar(xyC - zs);
+  fMat[1][1] = SkDoubleToScalar(y * yC + c);
+  fMat[1][2] = SkDoubleToScalar(yzC + xs);
+  fMat[1][3] = SkDoubleToScalar(0);
+  fMat[2][0] = SkDoubleToScalar(zxC + ys);
+  fMat[2][1] = SkDoubleToScalar(yzC - xs);
+  fMat[2][2] = SkDoubleToScalar(z * zC + c);
+  fMat[2][3] = SkDoubleToScalar(0);
+  fMat[3][0] = SkDoubleToScalar(0);
+  fMat[3][1] = SkDoubleToScalar(0);
+  fMat[3][2] = SkDoubleToScalar(0);
+  fMat[3][3] = SkDoubleToScalar(1);
 
-               SkScalar(xyC - zs),    // skew y
-               SkScalar(y * yC + c),  // scale y
-               SkScalar(yzC + xs),    // trans y
+  this->recomputeTypeMask();
+}
 
-               SkScalar(zxC + ys),     // persp x
-               SkScalar(yzC - xs),     // persp y
-               SkScalar(z * zC + c));  // persp 2
+void Matrix44::setRotateAboutXAxisSinCos(SkScalar sin_angle,
+                                         SkScalar cos_angle) {
+  fMat[0][0] = 1;
+  fMat[0][1] = 0;
+  fMat[0][2] = 0;
+  fMat[0][3] = 0;
+  fMat[1][0] = 0;
+  fMat[1][1] = cos_angle;
+  fMat[1][2] = sin_angle;
+  fMat[1][3] = 0;
+  fMat[2][0] = 0;
+  fMat[2][1] = -sin_angle;
+  fMat[2][2] = cos_angle;
+  fMat[2][3] = 0;
+  fMat[3][0] = 0;
+  fMat[3][1] = 0;
+  fMat[3][2] = 0;
+  fMat[3][3] = 1;
+
+  this->recomputeTypeMask();
+}
+
+void Matrix44::setRotateAboutYAxisSinCos(SkScalar sin_angle,
+                                         SkScalar cos_angle) {
+  fMat[0][0] = cos_angle;
+  fMat[0][1] = 0;
+  fMat[0][2] = -sin_angle;
+  fMat[0][3] = 0;
+  fMat[1][0] = 0;
+  fMat[1][1] = 1;
+  fMat[1][2] = 0;
+  fMat[1][3] = 0;
+  fMat[2][0] = sin_angle;
+  fMat[2][1] = 0;
+  fMat[2][2] = cos_angle;
+  fMat[2][3] = 0;
+  fMat[3][0] = 0;
+  fMat[3][1] = 0;
+  fMat[3][2] = 0;
+  fMat[3][3] = 1;
+
+  this->recomputeTypeMask();
+}
+
+void Matrix44::setRotateAboutZAxisSinCos(SkScalar sin_angle,
+                                         SkScalar cos_angle) {
+  fMat[0][0] = cos_angle;
+  fMat[0][1] = sin_angle;
+  fMat[0][2] = 0;
+  fMat[0][3] = 0;
+  fMat[1][0] = -sin_angle;
+  fMat[1][1] = cos_angle;
+  fMat[1][2] = 0;
+  fMat[1][3] = 0;
+  fMat[2][0] = 0;
+  fMat[2][1] = 0;
+  fMat[2][2] = 1;
+  fMat[2][3] = 0;
+  fMat[3][0] = 0;
+  fMat[3][1] = 0;
+  fMat[3][2] = 0;
+  fMat[3][3] = 1;
+
+  this->recomputeTypeMask();
 }
 
 ///////////////////////////////////////////////////////////////////////////////
diff --git a/ui/gfx/geometry/matrix44.h b/ui/gfx/geometry/matrix44.h
index bed499d..223031d 100644
--- a/ui/gfx/geometry/matrix44.h
+++ b/ui/gfx/geometry/matrix44.h
@@ -54,13 +54,6 @@
 
 // This is the underlying data structure of Transform. Don't use this type
 // directly. The public methods can be called through Transform::matrix().
-//
-// This class was originally SkMatrix44, then moved into Chromium as
-// skia::Matrix44, then moved here. For now this class mostly follows the
-// Skia coding style, especially the naming convention. This is to make the
-// API of this class similar to SkM44 to ease experiment with different
-// underlying matrix data structure of Transform.
-//
 class GEOMETRY_SKIA_EXPORT Matrix44 {
  public:
   enum Uninitialized_Constructor { kUninitialized_Constructor };
@@ -109,7 +102,7 @@
    * [ g h i ]      [ 0 0 1 0 ]
    *                [ g h 0 i ]
    */
-  explicit Matrix44(const SkMatrix& sk_matrix);
+  explicit Matrix44(const SkMatrix&);
 
   // Inverse conversion of the above.
   SkMatrix asM33() const;
@@ -216,20 +209,6 @@
   CATransform3D ToCATransform3D() const;
 #endif
 
-  /* This sets the top-left of the matrix and clears the translation and
-   * perspective components (with [3][3] set to 1).  m_ij is interpreted
-   * as the matrix entry at row = i, col = j. */
-  void set3x3(SkScalar m_00,
-              SkScalar m_10,
-              SkScalar m_20,
-              SkScalar m_01,
-              SkScalar m_11,
-              SkScalar m_21,
-              SkScalar m_02,
-              SkScalar m_12,
-              SkScalar m_22);
-  void set3x3RowMajorf(const float[]);
-
   Matrix44& setTranslate(SkScalar dx, SkScalar dy, SkScalar dz);
   Matrix44& preTranslate(SkScalar dx, SkScalar dy, SkScalar dz);
   Matrix44& postTranslate(SkScalar dx, SkScalar dy, SkScalar dz);
@@ -248,21 +227,20 @@
     return this->postScale(scale, scale, scale);
   }
 
-  void setRotateDegreesAbout(SkScalar x,
-                             SkScalar y,
-                             SkScalar z,
-                             SkScalar degrees) {
-    this->setRotateAbout(x, y, z, degrees * SK_ScalarPI / 180);
-  }
+  // Sets this matrix to rotate about the specified unit-length axis vector,
+  // by an angle specified by its sin() and cos(). This does not attempt to
+  // verify that axis(x, y, z).length() == 1 or that the sin, cos values are
+  // correct.
+  void setRotateUnitSinCos(SkScalar x,
+                           SkScalar y,
+                           SkScalar z,
+                           SkScalar sin_angle,
+                           SkScalar cos_angle);
 
-  /** Rotate about the vector [x,y,z]. If that vector is not unit-length,
-      it will be automatically resized.
-   */
-  void setRotateAbout(SkScalar x, SkScalar y, SkScalar z, SkScalar radians);
-  /** Rotate about the vector [x,y,z]. Does not check the length of the
-      vector, assuming it is unit-length.
-   */
-  void setRotateAboutUnit(SkScalar x, SkScalar y, SkScalar z, SkScalar radians);
+  // Special case for x, y or z axis of the above function.
+  void setRotateAboutXAxisSinCos(SkScalar sin_angle, SkScalar cos_angle);
+  void setRotateAboutYAxisSinCos(SkScalar sin_angle, SkScalar cos_angle);
+  void setRotateAboutZAxisSinCos(SkScalar sin_angle, SkScalar cos_angle);
 
   void setConcat(const Matrix44& a, const Matrix44& b);
   inline void preConcat(const Matrix44& m) { this->setConcat(*this, m); }
@@ -327,9 +305,6 @@
 
   static constexpr int kAllPublic_Masks = 0xF;
 
-  void as3x4RowMajorf(float[]) const;
-  void set3x4RowMajorf(const float[]);
-
   SkScalar transX() const { return fMat[3][0]; }
   SkScalar transY() const { return fMat[3][1]; }
   SkScalar transZ() const { return fMat[3][2]; }
diff --git a/ui/gfx/geometry/transform.cc b/ui/gfx/geometry/transform.cc
index c089721..2b6442e 100644
--- a/ui/gfx/geometry/transform.cc
+++ b/ui/gfx/geometry/transform.cc
@@ -79,74 +79,90 @@
   matrix_.setRowMajor(data);
 }
 
+// clang-format off
 Transform::Transform(const Quaternion& q)
-    : matrix_(Matrix44::kUninitialized_Constructor) {
-  double x = q.x();
-  double y = q.y();
-  double z = q.z();
-  double w = q.w();
-
-  // Implicitly calls matrix.setIdentity()
-  matrix_.set3x3(SkDoubleToScalar(1.0 - 2.0 * (y * y + z * z)),
-                 SkDoubleToScalar(2.0 * (x * y + z * w)),
-                 SkDoubleToScalar(2.0 * (x * z - y * w)),
-                 SkDoubleToScalar(2.0 * (x * y - z * w)),
-                 SkDoubleToScalar(1.0 - 2.0 * (x * x + z * z)),
-                 SkDoubleToScalar(2.0 * (y * z + x * w)),
-                 SkDoubleToScalar(2.0 * (x * z + y * w)),
-                 SkDoubleToScalar(2.0 * (y * z - x * w)),
-                 SkDoubleToScalar(1.0 - 2.0 * (x * x + y * y)));
-}
+    : matrix_(
+        // Row 1.
+        SkDoubleToScalar(1.0 - 2.0 * (q.y() * q.y() + q.z() * q.z())),
+        SkDoubleToScalar(2.0 * (q.x() * q.y() - q.z() * q.w())),
+        SkDoubleToScalar(2.0 * (q.x() * q.z() + q.y() * q.w())),
+        0,
+        // Row 2.
+        SkDoubleToScalar(2.0 * (q.x() * q.y() + q.z() * q.w())),
+        SkDoubleToScalar(1.0 - 2.0 * (q.x() * q.x() + q.z() * q.z())),
+        SkDoubleToScalar(2.0 * (q.y() * q.z() - q.x() * q.w())),
+        0,
+        // Row 3.
+        SkDoubleToScalar(2.0 * (q.x() * q.z() - q.y() * q.w())),
+        SkDoubleToScalar(2.0 * (q.y() * q.z() + q.x() * q.w())),
+        SkDoubleToScalar(1.0 - 2.0 * (q.x() * q.x() + q.y() * q.y())),
+        0,
+        // row 4.
+        0, 0, 0, 1) {}
+// clang-format on
 
 void Transform::RotateAboutXAxis(double degrees) {
   double radians = gfx::DegToRad(degrees);
-  SkScalar cosTheta = SkDoubleToScalar(std::cos(radians));
-  SkScalar sinTheta = SkDoubleToScalar(std::sin(radians));
+  SkScalar sin_theta = SkDoubleToScalar(std::sin(radians));
+  SkScalar cos_theta = SkDoubleToScalar(std::cos(radians));
   if (matrix_.isIdentity()) {
-    matrix_.set3x3(1, 0, 0, 0, cosTheta, sinTheta, 0, -sinTheta, cosTheta);
+    matrix_.setRotateAboutXAxisSinCos(sin_theta, cos_theta);
   } else {
     Matrix44 rot(Matrix44::kUninitialized_Constructor);
-    rot.set3x3(1, 0, 0, 0, cosTheta, sinTheta, 0, -sinTheta, cosTheta);
+    rot.setRotateAboutXAxisSinCos(sin_theta, cos_theta);
     matrix_.preConcat(rot);
   }
 }
 
 void Transform::RotateAboutYAxis(double degrees) {
   double radians = gfx::DegToRad(degrees);
-  SkScalar cosTheta = SkDoubleToScalar(std::cos(radians));
-  SkScalar sinTheta = SkDoubleToScalar(std::sin(radians));
+  SkScalar sin_theta = SkDoubleToScalar(std::sin(radians));
+  SkScalar cos_theta = SkDoubleToScalar(std::cos(radians));
   if (matrix_.isIdentity()) {
-    // Note carefully the placement of the -sinTheta for rotation about
-    // y-axis is different than rotation about x-axis or z-axis.
-    matrix_.set3x3(cosTheta, 0, -sinTheta, 0, 1, 0, sinTheta, 0, cosTheta);
+    matrix_.setRotateAboutYAxisSinCos(sin_theta, cos_theta);
   } else {
     Matrix44 rot(Matrix44::kUninitialized_Constructor);
-    rot.set3x3(cosTheta, 0, -sinTheta, 0, 1, 0, sinTheta, 0, cosTheta);
+    rot.setRotateAboutYAxisSinCos(sin_theta, cos_theta);
     matrix_.preConcat(rot);
   }
 }
 
 void Transform::RotateAboutZAxis(double degrees) {
   double radians = gfx::DegToRad(degrees);
-  SkScalar cosTheta = SkDoubleToScalar(std::cos(radians));
-  SkScalar sinTheta = SkDoubleToScalar(std::sin(radians));
+  SkScalar sin_theta = SkDoubleToScalar(std::sin(radians));
+  SkScalar cos_theta = SkDoubleToScalar(std::cos(radians));
   if (matrix_.isIdentity()) {
-    matrix_.set3x3(cosTheta, sinTheta, 0, -sinTheta, cosTheta, 0, 0, 0, 1);
+    matrix_.setRotateAboutZAxisSinCos(sin_theta, cos_theta);
   } else {
     Matrix44 rot(Matrix44::kUninitialized_Constructor);
-    rot.set3x3(cosTheta, sinTheta, 0, -sinTheta, cosTheta, 0, 0, 0, 1);
+    rot.setRotateAboutZAxisSinCos(sin_theta, cos_theta);
     matrix_.preConcat(rot);
   }
 }
 
 void Transform::RotateAbout(const Vector3dF& axis, double degrees) {
+  double x = axis.x();
+  double y = axis.y();
+  double z = axis.z();
+  double square_length = x * x + y * y + z * z;
+  if (square_length == 0)
+    return;
+  if (square_length != 1) {
+    double scale = 1 / sqrt(square_length);
+    x *= scale;
+    y *= scale;
+    z *= scale;
+  }
+  double radians = gfx::DegToRad(degrees);
+  SkScalar sin_theta = SkDoubleToScalar(std::sin(radians));
+  SkScalar cos_theta = SkDoubleToScalar(std::cos(radians));
   if (matrix_.isIdentity()) {
-    matrix_.setRotateDegreesAbout(axis.x(), axis.y(), axis.z(),
-                                  SkDoubleToScalar(degrees));
+    matrix_.setRotateUnitSinCos(SkDoubleToScalar(x), SkDoubleToScalar(y),
+                                SkDoubleToScalar(z), sin_theta, cos_theta);
   } else {
     Matrix44 rot(Matrix44::kUninitialized_Constructor);
-    rot.setRotateDegreesAbout(axis.x(), axis.y(), axis.z(),
-                              SkDoubleToScalar(degrees));
+    rot.setRotateUnitSinCos(SkDoubleToScalar(x), SkDoubleToScalar(y),
+                            SkDoubleToScalar(z), sin_theta, cos_theta);
     matrix_.preConcat(rot);
   }
 }
diff --git a/ui/gfx/geometry/transform_unittest.cc b/ui/gfx/geometry/transform_unittest.cc
index 0ca9c0c..eef7a30 100644
--- a/ui/gfx/geometry/transform_unittest.cc
+++ b/ui/gfx/geometry/transform_unittest.cc
@@ -169,8 +169,8 @@
 #define LOOSE_ERROR_THRESHOLD 1e-7
 
 TEST(XFormTest, Equality) {
-  Transform lhs, rhs, interpolated;
-  rhs.matrix().set3x3(1, 2, 3, 4, 5, 6, 7, 8, 9);
+  Transform lhs, interpolated;
+  Transform rhs(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16);
   interpolated = lhs;
   for (int i = 0; i <= 100; ++i) {
     for (int row = 0; row < 4; ++row) {
@@ -1397,6 +1397,14 @@
   EXPECT_ROW4_EQ(0.0f, 0.0f, 0.0f, 1.0f, transform);
 }
 
+TEST(XFormTest, FromQuaternion) {
+  Transform t(Quaternion(1, 2, 3, 4));
+  EXPECT_ROW1_EQ(-25.f, -20.f, 22.f, 0.f, t);
+  EXPECT_ROW2_EQ(28.f, -19.f, 4.f, 0.f, t);
+  EXPECT_ROW3_EQ(-10.f, 20.f, -9.f, 0.f, t);
+  EXPECT_ROW4_EQ(0.f, 0.f, 0.f, 1.f, t);
+}
+
 TEST(XFormTest, verifyAssignmentOperator) {
   Transform A;
   InitializeTestMatrix(&A);
@@ -2663,7 +2671,7 @@
   EXPECT_EQ(expected.ToString(), rrect.ToString());
 
   Matrix44 rot(Matrix44::kUninitialized_Constructor);
-  rot.set3x3(0, 1, 0, -1, 0, 0, 0, 0, 1);
+  rot.setRotateAboutZAxisSinCos(1, 0);
   Transform rotation_90_Clock(rot);
 
   rrect = RRectF(gfx::RectF(0, 0, 20.f, 25.f),
diff --git a/ui/gfx/interpolated_transform.cc b/ui/gfx/interpolated_transform.cc
index e92f587..81376f61 100644
--- a/ui/gfx/interpolated_transform.cc
+++ b/ui/gfx/interpolated_transform.cc
@@ -41,17 +41,11 @@
   // n should now be in the range [0, 3]
   // clang-format off
   if (n == 1) {
-    transform.matrix().set3x3( 0,  1,  0,
-                              -1,  0,  0,
-                               0,  0,  1);
+    transform.matrix().setRotateAboutZAxisSinCos(1, 0);
   } else if (n == 2) {
-    transform.matrix().set3x3(-1,  0,  0,
-                               0, -1,  0,
-                               0,  0,  1);
+    transform.matrix().setRotateAboutZAxisSinCos(0, -1);
   } else if (n == 3) {
-    transform.matrix().set3x3( 0, -1,  0,
-                               1,  0,  0,
-                               0,  0,  1);
+    transform.matrix().setRotateAboutZAxisSinCos(-1, 0);
   }
   // clang-format on
 
diff --git a/ui/gfx/x/generated_protos/damage.cc b/ui/gfx/x/generated_protos/damage.cc
index 81b2748..9504493 100644
--- a/ui/gfx/x/generated_protos/damage.cc
+++ b/ui/gfx/x/generated_protos/damage.cc
@@ -56,7 +56,10 @@
 std::string Damage::BadDamageError::ToString() const {
   std::stringstream ss_;
   ss_ << "Damage::BadDamageError{";
-  ss_ << ".sequence = " << static_cast<uint64_t>(sequence);
+  ss_ << ".sequence = " << static_cast<uint64_t>(sequence) << ", ";
+  ss_ << ".bad_value = " << static_cast<uint64_t>(bad_value) << ", ";
+  ss_ << ".minor_opcode = " << static_cast<uint64_t>(minor_opcode) << ", ";
+  ss_ << ".major_opcode = " << static_cast<uint64_t>(major_opcode);
   ss_ << "}";
   return ss_.str();
 }
@@ -67,6 +70,9 @@
   auto& buf = *buffer;
 
   auto& sequence = (*error_).sequence;
+  auto& bad_value = (*error_).bad_value;
+  auto& minor_opcode = (*error_).minor_opcode;
+  auto& major_opcode = (*error_).major_opcode;
 
   // response_type
   uint8_t response_type;
@@ -79,6 +85,15 @@
   // sequence
   Read(&sequence, &buf);
 
+  // bad_value
+  Read(&bad_value, &buf);
+
+  // minor_opcode
+  Read(&minor_opcode, &buf);
+
+  // major_opcode
+  Read(&major_opcode, &buf);
+
   DCHECK_LE(buf.offset, 32ul);
 }
 template <>
diff --git a/ui/gfx/x/generated_protos/damage.h b/ui/gfx/x/generated_protos/damage.h
index 581e8a6..ceada070 100644
--- a/ui/gfx/x/generated_protos/damage.h
+++ b/ui/gfx/x/generated_protos/damage.h
@@ -92,6 +92,9 @@
 
   struct BadDamageError : public x11::Error {
     uint16_t sequence{};
+    uint32_t bad_value{};
+    uint16_t minor_opcode{};
+    uint8_t major_opcode{};
 
     std::string ToString() const override;
   };
diff --git a/ui/gfx/x/generated_protos/glx.cc b/ui/gfx/x/generated_protos/glx.cc
index f0adfb92..dfb2fb3 100644
--- a/ui/gfx/x/generated_protos/glx.cc
+++ b/ui/gfx/x/generated_protos/glx.cc
@@ -2797,6 +2797,9 @@
     buf.Write(&gl_extension_string_elem);
   }
 
+  // pad0
+  Align(&buf, 4);
+
   // glx_extension_string
   DCHECK_EQ(static_cast<size_t>(glx_str_len), glx_extension_string.size());
   for (auto& glx_extension_string_elem : glx_extension_string) {
@@ -2956,6 +2959,9 @@
     buf.Write(&gl_extension_string_elem);
   }
 
+  // pad0
+  Align(&buf, 4);
+
   // glx_extension_string
   DCHECK_EQ(static_cast<size_t>(glx_str_len), glx_extension_string.size());
   for (auto& glx_extension_string_elem : glx_extension_string) {
diff --git a/ui/gfx/x/generated_protos/randr.cc b/ui/gfx/x/generated_protos/randr.cc
index 1b83849a..efa0905 100644
--- a/ui/gfx/x/generated_protos/randr.cc
+++ b/ui/gfx/x/generated_protos/randr.cc
@@ -56,7 +56,10 @@
 std::string RandR::BadOutputError::ToString() const {
   std::stringstream ss_;
   ss_ << "RandR::BadOutputError{";
-  ss_ << ".sequence = " << static_cast<uint64_t>(sequence);
+  ss_ << ".sequence = " << static_cast<uint64_t>(sequence) << ", ";
+  ss_ << ".bad_value = " << static_cast<uint64_t>(bad_value) << ", ";
+  ss_ << ".minor_opcode = " << static_cast<uint64_t>(minor_opcode) << ", ";
+  ss_ << ".major_opcode = " << static_cast<uint64_t>(major_opcode);
   ss_ << "}";
   return ss_.str();
 }
@@ -67,6 +70,9 @@
   auto& buf = *buffer;
 
   auto& sequence = (*error_).sequence;
+  auto& bad_value = (*error_).bad_value;
+  auto& minor_opcode = (*error_).minor_opcode;
+  auto& major_opcode = (*error_).major_opcode;
 
   // response_type
   uint8_t response_type;
@@ -79,12 +85,24 @@
   // sequence
   Read(&sequence, &buf);
 
+  // bad_value
+  Read(&bad_value, &buf);
+
+  // minor_opcode
+  Read(&minor_opcode, &buf);
+
+  // major_opcode
+  Read(&major_opcode, &buf);
+
   DCHECK_LE(buf.offset, 32ul);
 }
 std::string RandR::BadCrtcError::ToString() const {
   std::stringstream ss_;
   ss_ << "RandR::BadCrtcError{";
-  ss_ << ".sequence = " << static_cast<uint64_t>(sequence);
+  ss_ << ".sequence = " << static_cast<uint64_t>(sequence) << ", ";
+  ss_ << ".bad_value = " << static_cast<uint64_t>(bad_value) << ", ";
+  ss_ << ".minor_opcode = " << static_cast<uint64_t>(minor_opcode) << ", ";
+  ss_ << ".major_opcode = " << static_cast<uint64_t>(major_opcode);
   ss_ << "}";
   return ss_.str();
 }
@@ -95,6 +113,9 @@
   auto& buf = *buffer;
 
   auto& sequence = (*error_).sequence;
+  auto& bad_value = (*error_).bad_value;
+  auto& minor_opcode = (*error_).minor_opcode;
+  auto& major_opcode = (*error_).major_opcode;
 
   // response_type
   uint8_t response_type;
@@ -107,12 +128,24 @@
   // sequence
   Read(&sequence, &buf);
 
+  // bad_value
+  Read(&bad_value, &buf);
+
+  // minor_opcode
+  Read(&minor_opcode, &buf);
+
+  // major_opcode
+  Read(&major_opcode, &buf);
+
   DCHECK_LE(buf.offset, 32ul);
 }
 std::string RandR::BadModeError::ToString() const {
   std::stringstream ss_;
   ss_ << "RandR::BadModeError{";
-  ss_ << ".sequence = " << static_cast<uint64_t>(sequence);
+  ss_ << ".sequence = " << static_cast<uint64_t>(sequence) << ", ";
+  ss_ << ".bad_value = " << static_cast<uint64_t>(bad_value) << ", ";
+  ss_ << ".minor_opcode = " << static_cast<uint64_t>(minor_opcode) << ", ";
+  ss_ << ".major_opcode = " << static_cast<uint64_t>(major_opcode);
   ss_ << "}";
   return ss_.str();
 }
@@ -123,6 +156,9 @@
   auto& buf = *buffer;
 
   auto& sequence = (*error_).sequence;
+  auto& bad_value = (*error_).bad_value;
+  auto& minor_opcode = (*error_).minor_opcode;
+  auto& major_opcode = (*error_).major_opcode;
 
   // response_type
   uint8_t response_type;
@@ -135,12 +171,24 @@
   // sequence
   Read(&sequence, &buf);
 
+  // bad_value
+  Read(&bad_value, &buf);
+
+  // minor_opcode
+  Read(&minor_opcode, &buf);
+
+  // major_opcode
+  Read(&major_opcode, &buf);
+
   DCHECK_LE(buf.offset, 32ul);
 }
 std::string RandR::BadProviderError::ToString() const {
   std::stringstream ss_;
   ss_ << "RandR::BadProviderError{";
-  ss_ << ".sequence = " << static_cast<uint64_t>(sequence);
+  ss_ << ".sequence = " << static_cast<uint64_t>(sequence) << ", ";
+  ss_ << ".bad_value = " << static_cast<uint64_t>(bad_value) << ", ";
+  ss_ << ".minor_opcode = " << static_cast<uint64_t>(minor_opcode) << ", ";
+  ss_ << ".major_opcode = " << static_cast<uint64_t>(major_opcode);
   ss_ << "}";
   return ss_.str();
 }
@@ -151,6 +199,9 @@
   auto& buf = *buffer;
 
   auto& sequence = (*error_).sequence;
+  auto& bad_value = (*error_).bad_value;
+  auto& minor_opcode = (*error_).minor_opcode;
+  auto& major_opcode = (*error_).major_opcode;
 
   // response_type
   uint8_t response_type;
@@ -163,6 +214,15 @@
   // sequence
   Read(&sequence, &buf);
 
+  // bad_value
+  Read(&bad_value, &buf);
+
+  // minor_opcode
+  Read(&minor_opcode, &buf);
+
+  // major_opcode
+  Read(&major_opcode, &buf);
+
   DCHECK_LE(buf.offset, 32ul);
 }
 template <>
diff --git a/ui/gfx/x/generated_protos/randr.h b/ui/gfx/x/generated_protos/randr.h
index b79aa9a..97839536 100644
--- a/ui/gfx/x/generated_protos/randr.h
+++ b/ui/gfx/x/generated_protos/randr.h
@@ -167,24 +167,36 @@
 
   struct BadOutputError : public x11::Error {
     uint16_t sequence{};
+    uint32_t bad_value{};
+    uint16_t minor_opcode{};
+    uint8_t major_opcode{};
 
     std::string ToString() const override;
   };
 
   struct BadCrtcError : public x11::Error {
     uint16_t sequence{};
+    uint32_t bad_value{};
+    uint16_t minor_opcode{};
+    uint8_t major_opcode{};
 
     std::string ToString() const override;
   };
 
   struct BadModeError : public x11::Error {
     uint16_t sequence{};
+    uint32_t bad_value{};
+    uint16_t minor_opcode{};
+    uint8_t major_opcode{};
 
     std::string ToString() const override;
   };
 
   struct BadProviderError : public x11::Error {
     uint16_t sequence{};
+    uint32_t bad_value{};
+    uint16_t minor_opcode{};
+    uint8_t major_opcode{};
 
     std::string ToString() const override;
   };
diff --git a/ui/gfx/x/generated_protos/read_event.cc b/ui/gfx/x/generated_protos/read_event.cc
index bc58a45c..ec344cf9 100644
--- a/ui/gfx/x/generated_protos/read_event.cc
+++ b/ui/gfx/x/generated_protos/read_event.cc
@@ -622,9 +622,43 @@
     return;
   }
 
+  if (evtype == GeGenericEvent::opcode && conn->xinput().present() &&
+      ge->extension == conn->xinput().major_opcode() &&
+      (ge->event_type == Input::GesturePinchEvent::Begin ||
+       ge->event_type == Input::GesturePinchEvent::Update ||
+       ge->event_type == Input::GesturePinchEvent::End)) {
+    event->type_id_ = 38;
+    event->deleter_ = [](void* event) {
+      delete reinterpret_cast<Input::GesturePinchEvent*>(event);
+    };
+    auto* event_ = new Input::GesturePinchEvent;
+    ReadEvent(event_, buffer);
+    event_->opcode = static_cast<decltype(event_->opcode)>(ge->event_type);
+    event->event_ = event_;
+    event->window_ = event_->GetWindow();
+    return;
+  }
+
+  if (evtype == GeGenericEvent::opcode && conn->xinput().present() &&
+      ge->extension == conn->xinput().major_opcode() &&
+      (ge->event_type == Input::GestureSwipeEvent::Begin ||
+       ge->event_type == Input::GestureSwipeEvent::Update ||
+       ge->event_type == Input::GestureSwipeEvent::End)) {
+    event->type_id_ = 39;
+    event->deleter_ = [](void* event) {
+      delete reinterpret_cast<Input::GestureSwipeEvent*>(event);
+    };
+    auto* event_ = new Input::GestureSwipeEvent;
+    ReadEvent(event_, buffer);
+    event_->opcode = static_cast<decltype(event_->opcode)>(ge->event_type);
+    event->event_ = event_;
+    event->window_ = event_->GetWindow();
+    return;
+  }
+
   if (conn->xkb().present() && evtype - conn->xkb().first_event() ==
                                    Xkb::NewKeyboardNotifyEvent::opcode) {
-    event->type_id_ = 38;
+    event->type_id_ = 40;
     event->deleter_ = [](void* event) {
       delete reinterpret_cast<Xkb::NewKeyboardNotifyEvent*>(event);
     };
@@ -637,7 +671,7 @@
 
   if (conn->xkb().present() &&
       evtype - conn->xkb().first_event() == Xkb::MapNotifyEvent::opcode) {
-    event->type_id_ = 39;
+    event->type_id_ = 41;
     event->deleter_ = [](void* event) {
       delete reinterpret_cast<Xkb::MapNotifyEvent*>(event);
     };
@@ -650,7 +684,7 @@
 
   if (conn->xkb().present() &&
       evtype - conn->xkb().first_event() == Xkb::StateNotifyEvent::opcode) {
-    event->type_id_ = 40;
+    event->type_id_ = 42;
     event->deleter_ = [](void* event) {
       delete reinterpret_cast<Xkb::StateNotifyEvent*>(event);
     };
@@ -663,7 +697,7 @@
 
   if (conn->xkb().present() &&
       evtype - conn->xkb().first_event() == Xkb::ControlsNotifyEvent::opcode) {
-    event->type_id_ = 41;
+    event->type_id_ = 43;
     event->deleter_ = [](void* event) {
       delete reinterpret_cast<Xkb::ControlsNotifyEvent*>(event);
     };
@@ -676,7 +710,7 @@
 
   if (conn->xkb().present() && evtype - conn->xkb().first_event() ==
                                    Xkb::IndicatorStateNotifyEvent::opcode) {
-    event->type_id_ = 42;
+    event->type_id_ = 44;
     event->deleter_ = [](void* event) {
       delete reinterpret_cast<Xkb::IndicatorStateNotifyEvent*>(event);
     };
@@ -689,7 +723,7 @@
 
   if (conn->xkb().present() && evtype - conn->xkb().first_event() ==
                                    Xkb::IndicatorMapNotifyEvent::opcode) {
-    event->type_id_ = 43;
+    event->type_id_ = 45;
     event->deleter_ = [](void* event) {
       delete reinterpret_cast<Xkb::IndicatorMapNotifyEvent*>(event);
     };
@@ -702,7 +736,7 @@
 
   if (conn->xkb().present() &&
       evtype - conn->xkb().first_event() == Xkb::NamesNotifyEvent::opcode) {
-    event->type_id_ = 44;
+    event->type_id_ = 46;
     event->deleter_ = [](void* event) {
       delete reinterpret_cast<Xkb::NamesNotifyEvent*>(event);
     };
@@ -715,7 +749,7 @@
 
   if (conn->xkb().present() &&
       evtype - conn->xkb().first_event() == Xkb::CompatMapNotifyEvent::opcode) {
-    event->type_id_ = 45;
+    event->type_id_ = 47;
     event->deleter_ = [](void* event) {
       delete reinterpret_cast<Xkb::CompatMapNotifyEvent*>(event);
     };
@@ -728,7 +762,7 @@
 
   if (conn->xkb().present() &&
       evtype - conn->xkb().first_event() == Xkb::BellNotifyEvent::opcode) {
-    event->type_id_ = 46;
+    event->type_id_ = 48;
     event->deleter_ = [](void* event) {
       delete reinterpret_cast<Xkb::BellNotifyEvent*>(event);
     };
@@ -741,7 +775,7 @@
 
   if (conn->xkb().present() &&
       evtype - conn->xkb().first_event() == Xkb::ActionMessageEvent::opcode) {
-    event->type_id_ = 47;
+    event->type_id_ = 49;
     event->deleter_ = [](void* event) {
       delete reinterpret_cast<Xkb::ActionMessageEvent*>(event);
     };
@@ -754,7 +788,7 @@
 
   if (conn->xkb().present() &&
       evtype - conn->xkb().first_event() == Xkb::AccessXNotifyEvent::opcode) {
-    event->type_id_ = 48;
+    event->type_id_ = 50;
     event->deleter_ = [](void* event) {
       delete reinterpret_cast<Xkb::AccessXNotifyEvent*>(event);
     };
@@ -767,7 +801,7 @@
 
   if (conn->xkb().present() && evtype - conn->xkb().first_event() ==
                                    Xkb::ExtensionDeviceNotifyEvent::opcode) {
-    event->type_id_ = 49;
+    event->type_id_ = 51;
     event->deleter_ = [](void* event) {
       delete reinterpret_cast<Xkb::ExtensionDeviceNotifyEvent*>(event);
     };
@@ -780,7 +814,7 @@
 
   if (conn->xprint().present() &&
       evtype - conn->xprint().first_event() == XPrint::NotifyEvent::opcode) {
-    event->type_id_ = 50;
+    event->type_id_ = 52;
     event->deleter_ = [](void* event) {
       delete reinterpret_cast<XPrint::NotifyEvent*>(event);
     };
@@ -793,7 +827,7 @@
 
   if (conn->xprint().present() && evtype - conn->xprint().first_event() ==
                                       XPrint::AttributNotifyEvent::opcode) {
-    event->type_id_ = 51;
+    event->type_id_ = 53;
     event->deleter_ = [](void* event) {
       delete reinterpret_cast<XPrint::AttributNotifyEvent*>(event);
     };
@@ -805,7 +839,7 @@
   }
 
   if ((evtype == KeyEvent::Press || evtype == KeyEvent::Release)) {
-    event->type_id_ = 52;
+    event->type_id_ = 54;
     event->deleter_ = [](void* event) {
       delete reinterpret_cast<KeyEvent*>(event);
     };
@@ -818,7 +852,7 @@
   }
 
   if ((evtype == ButtonEvent::Press || evtype == ButtonEvent::Release)) {
-    event->type_id_ = 53;
+    event->type_id_ = 55;
     event->deleter_ = [](void* event) {
       delete reinterpret_cast<ButtonEvent*>(event);
     };
@@ -831,7 +865,7 @@
   }
 
   if (evtype == MotionNotifyEvent::opcode) {
-    event->type_id_ = 54;
+    event->type_id_ = 56;
     event->deleter_ = [](void* event) {
       delete reinterpret_cast<MotionNotifyEvent*>(event);
     };
@@ -844,7 +878,7 @@
 
   if ((evtype == CrossingEvent::EnterNotify ||
        evtype == CrossingEvent::LeaveNotify)) {
-    event->type_id_ = 55;
+    event->type_id_ = 57;
     event->deleter_ = [](void* event) {
       delete reinterpret_cast<CrossingEvent*>(event);
     };
@@ -857,7 +891,7 @@
   }
 
   if ((evtype == FocusEvent::In || evtype == FocusEvent::Out)) {
-    event->type_id_ = 56;
+    event->type_id_ = 58;
     event->deleter_ = [](void* event) {
       delete reinterpret_cast<FocusEvent*>(event);
     };
@@ -870,7 +904,7 @@
   }
 
   if (evtype == KeymapNotifyEvent::opcode) {
-    event->type_id_ = 57;
+    event->type_id_ = 59;
     event->deleter_ = [](void* event) {
       delete reinterpret_cast<KeymapNotifyEvent*>(event);
     };
@@ -882,7 +916,7 @@
   }
 
   if (evtype == ExposeEvent::opcode) {
-    event->type_id_ = 58;
+    event->type_id_ = 60;
     event->deleter_ = [](void* event) {
       delete reinterpret_cast<ExposeEvent*>(event);
     };
@@ -894,7 +928,7 @@
   }
 
   if (evtype == GraphicsExposureEvent::opcode) {
-    event->type_id_ = 59;
+    event->type_id_ = 61;
     event->deleter_ = [](void* event) {
       delete reinterpret_cast<GraphicsExposureEvent*>(event);
     };
@@ -906,7 +940,7 @@
   }
 
   if (evtype == NoExposureEvent::opcode) {
-    event->type_id_ = 60;
+    event->type_id_ = 62;
     event->deleter_ = [](void* event) {
       delete reinterpret_cast<NoExposureEvent*>(event);
     };
@@ -918,7 +952,7 @@
   }
 
   if (evtype == VisibilityNotifyEvent::opcode) {
-    event->type_id_ = 61;
+    event->type_id_ = 63;
     event->deleter_ = [](void* event) {
       delete reinterpret_cast<VisibilityNotifyEvent*>(event);
     };
@@ -930,7 +964,7 @@
   }
 
   if (evtype == CreateNotifyEvent::opcode) {
-    event->type_id_ = 62;
+    event->type_id_ = 64;
     event->deleter_ = [](void* event) {
       delete reinterpret_cast<CreateNotifyEvent*>(event);
     };
@@ -942,7 +976,7 @@
   }
 
   if (evtype == DestroyNotifyEvent::opcode) {
-    event->type_id_ = 63;
+    event->type_id_ = 65;
     event->deleter_ = [](void* event) {
       delete reinterpret_cast<DestroyNotifyEvent*>(event);
     };
@@ -954,7 +988,7 @@
   }
 
   if (evtype == UnmapNotifyEvent::opcode) {
-    event->type_id_ = 64;
+    event->type_id_ = 66;
     event->deleter_ = [](void* event) {
       delete reinterpret_cast<UnmapNotifyEvent*>(event);
     };
@@ -966,7 +1000,7 @@
   }
 
   if (evtype == MapNotifyEvent::opcode) {
-    event->type_id_ = 65;
+    event->type_id_ = 67;
     event->deleter_ = [](void* event) {
       delete reinterpret_cast<MapNotifyEvent*>(event);
     };
@@ -978,7 +1012,7 @@
   }
 
   if (evtype == MapRequestEvent::opcode) {
-    event->type_id_ = 66;
+    event->type_id_ = 68;
     event->deleter_ = [](void* event) {
       delete reinterpret_cast<MapRequestEvent*>(event);
     };
@@ -990,7 +1024,7 @@
   }
 
   if (evtype == ReparentNotifyEvent::opcode) {
-    event->type_id_ = 67;
+    event->type_id_ = 69;
     event->deleter_ = [](void* event) {
       delete reinterpret_cast<ReparentNotifyEvent*>(event);
     };
@@ -1002,7 +1036,7 @@
   }
 
   if (evtype == ConfigureNotifyEvent::opcode) {
-    event->type_id_ = 68;
+    event->type_id_ = 70;
     event->deleter_ = [](void* event) {
       delete reinterpret_cast<ConfigureNotifyEvent*>(event);
     };
@@ -1014,7 +1048,7 @@
   }
 
   if (evtype == ConfigureRequestEvent::opcode) {
-    event->type_id_ = 69;
+    event->type_id_ = 71;
     event->deleter_ = [](void* event) {
       delete reinterpret_cast<ConfigureRequestEvent*>(event);
     };
@@ -1026,7 +1060,7 @@
   }
 
   if (evtype == GravityNotifyEvent::opcode) {
-    event->type_id_ = 70;
+    event->type_id_ = 72;
     event->deleter_ = [](void* event) {
       delete reinterpret_cast<GravityNotifyEvent*>(event);
     };
@@ -1038,7 +1072,7 @@
   }
 
   if (evtype == ResizeRequestEvent::opcode) {
-    event->type_id_ = 71;
+    event->type_id_ = 73;
     event->deleter_ = [](void* event) {
       delete reinterpret_cast<ResizeRequestEvent*>(event);
     };
@@ -1050,7 +1084,7 @@
   }
 
   if ((evtype == CirculateEvent::Notify || evtype == CirculateEvent::Request)) {
-    event->type_id_ = 72;
+    event->type_id_ = 74;
     event->deleter_ = [](void* event) {
       delete reinterpret_cast<CirculateEvent*>(event);
     };
@@ -1063,7 +1097,7 @@
   }
 
   if (evtype == PropertyNotifyEvent::opcode) {
-    event->type_id_ = 73;
+    event->type_id_ = 75;
     event->deleter_ = [](void* event) {
       delete reinterpret_cast<PropertyNotifyEvent*>(event);
     };
@@ -1075,7 +1109,7 @@
   }
 
   if (evtype == SelectionClearEvent::opcode) {
-    event->type_id_ = 74;
+    event->type_id_ = 76;
     event->deleter_ = [](void* event) {
       delete reinterpret_cast<SelectionClearEvent*>(event);
     };
@@ -1087,7 +1121,7 @@
   }
 
   if (evtype == SelectionRequestEvent::opcode) {
-    event->type_id_ = 75;
+    event->type_id_ = 77;
     event->deleter_ = [](void* event) {
       delete reinterpret_cast<SelectionRequestEvent*>(event);
     };
@@ -1099,7 +1133,7 @@
   }
 
   if (evtype == SelectionNotifyEvent::opcode) {
-    event->type_id_ = 76;
+    event->type_id_ = 78;
     event->deleter_ = [](void* event) {
       delete reinterpret_cast<SelectionNotifyEvent*>(event);
     };
@@ -1111,7 +1145,7 @@
   }
 
   if (evtype == ColormapNotifyEvent::opcode) {
-    event->type_id_ = 77;
+    event->type_id_ = 79;
     event->deleter_ = [](void* event) {
       delete reinterpret_cast<ColormapNotifyEvent*>(event);
     };
@@ -1123,7 +1157,7 @@
   }
 
   if (evtype == ClientMessageEvent::opcode) {
-    event->type_id_ = 78;
+    event->type_id_ = 80;
     event->deleter_ = [](void* event) {
       delete reinterpret_cast<ClientMessageEvent*>(event);
     };
@@ -1135,7 +1169,7 @@
   }
 
   if (evtype == MappingNotifyEvent::opcode) {
-    event->type_id_ = 79;
+    event->type_id_ = 81;
     event->deleter_ = [](void* event) {
       delete reinterpret_cast<MappingNotifyEvent*>(event);
     };
@@ -1148,7 +1182,7 @@
 
   if (conn->xv().present() &&
       evtype - conn->xv().first_event() == Xv::VideoNotifyEvent::opcode) {
-    event->type_id_ = 81;
+    event->type_id_ = 83;
     event->deleter_ = [](void* event) {
       delete reinterpret_cast<Xv::VideoNotifyEvent*>(event);
     };
@@ -1161,7 +1195,7 @@
 
   if (conn->xv().present() &&
       evtype - conn->xv().first_event() == Xv::PortNotifyEvent::opcode) {
-    event->type_id_ = 82;
+    event->type_id_ = 84;
     event->deleter_ = [](void* event) {
       delete reinterpret_cast<Xv::PortNotifyEvent*>(event);
     };
diff --git a/ui/gfx/x/generated_protos/record.cc b/ui/gfx/x/generated_protos/record.cc
index 356cce15..7465cae 100644
--- a/ui/gfx/x/generated_protos/record.cc
+++ b/ui/gfx/x/generated_protos/record.cc
@@ -57,7 +57,9 @@
   std::stringstream ss_;
   ss_ << "Record::BadContextError{";
   ss_ << ".sequence = " << static_cast<uint64_t>(sequence) << ", ";
-  ss_ << ".invalid_record = " << static_cast<uint64_t>(invalid_record);
+  ss_ << ".invalid_record = " << static_cast<uint64_t>(invalid_record) << ", ";
+  ss_ << ".minor_opcode = " << static_cast<uint64_t>(minor_opcode) << ", ";
+  ss_ << ".major_opcode = " << static_cast<uint64_t>(major_opcode);
   ss_ << "}";
   return ss_.str();
 }
@@ -69,6 +71,8 @@
 
   auto& sequence = (*error_).sequence;
   auto& invalid_record = (*error_).invalid_record;
+  auto& minor_opcode = (*error_).minor_opcode;
+  auto& major_opcode = (*error_).major_opcode;
 
   // response_type
   uint8_t response_type;
@@ -84,6 +88,12 @@
   // invalid_record
   Read(&invalid_record, &buf);
 
+  // minor_opcode
+  Read(&minor_opcode, &buf);
+
+  // major_opcode
+  Read(&major_opcode, &buf);
+
   DCHECK_LE(buf.offset, 32ul);
 }
 Future<Record::QueryVersionReply> Record::QueryVersion(
diff --git a/ui/gfx/x/generated_protos/record.h b/ui/gfx/x/generated_protos/record.h
index bc45d19..dccb50e 100644
--- a/ui/gfx/x/generated_protos/record.h
+++ b/ui/gfx/x/generated_protos/record.h
@@ -158,6 +158,8 @@
   struct BadContextError : public x11::Error {
     uint16_t sequence{};
     uint32_t invalid_record{};
+    uint16_t minor_opcode{};
+    uint8_t major_opcode{};
 
     std::string ToString() const override;
   };
diff --git a/ui/gfx/x/generated_protos/render.cc b/ui/gfx/x/generated_protos/render.cc
index 7784a7b..6910596 100644
--- a/ui/gfx/x/generated_protos/render.cc
+++ b/ui/gfx/x/generated_protos/render.cc
@@ -56,7 +56,10 @@
 std::string Render::PictFormatError::ToString() const {
   std::stringstream ss_;
   ss_ << "Render::PictFormatError{";
-  ss_ << ".sequence = " << static_cast<uint64_t>(sequence);
+  ss_ << ".sequence = " << static_cast<uint64_t>(sequence) << ", ";
+  ss_ << ".bad_value = " << static_cast<uint64_t>(bad_value) << ", ";
+  ss_ << ".minor_opcode = " << static_cast<uint64_t>(minor_opcode) << ", ";
+  ss_ << ".major_opcode = " << static_cast<uint64_t>(major_opcode);
   ss_ << "}";
   return ss_.str();
 }
@@ -67,6 +70,9 @@
   auto& buf = *buffer;
 
   auto& sequence = (*error_).sequence;
+  auto& bad_value = (*error_).bad_value;
+  auto& minor_opcode = (*error_).minor_opcode;
+  auto& major_opcode = (*error_).major_opcode;
 
   // response_type
   uint8_t response_type;
@@ -79,12 +85,24 @@
   // sequence
   Read(&sequence, &buf);
 
+  // bad_value
+  Read(&bad_value, &buf);
+
+  // minor_opcode
+  Read(&minor_opcode, &buf);
+
+  // major_opcode
+  Read(&major_opcode, &buf);
+
   DCHECK_LE(buf.offset, 32ul);
 }
 std::string Render::PictureError::ToString() const {
   std::stringstream ss_;
   ss_ << "Render::PictureError{";
-  ss_ << ".sequence = " << static_cast<uint64_t>(sequence);
+  ss_ << ".sequence = " << static_cast<uint64_t>(sequence) << ", ";
+  ss_ << ".bad_value = " << static_cast<uint64_t>(bad_value) << ", ";
+  ss_ << ".minor_opcode = " << static_cast<uint64_t>(minor_opcode) << ", ";
+  ss_ << ".major_opcode = " << static_cast<uint64_t>(major_opcode);
   ss_ << "}";
   return ss_.str();
 }
@@ -95,6 +113,9 @@
   auto& buf = *buffer;
 
   auto& sequence = (*error_).sequence;
+  auto& bad_value = (*error_).bad_value;
+  auto& minor_opcode = (*error_).minor_opcode;
+  auto& major_opcode = (*error_).major_opcode;
 
   // response_type
   uint8_t response_type;
@@ -107,12 +128,24 @@
   // sequence
   Read(&sequence, &buf);
 
+  // bad_value
+  Read(&bad_value, &buf);
+
+  // minor_opcode
+  Read(&minor_opcode, &buf);
+
+  // major_opcode
+  Read(&major_opcode, &buf);
+
   DCHECK_LE(buf.offset, 32ul);
 }
 std::string Render::PictOpError::ToString() const {
   std::stringstream ss_;
   ss_ << "Render::PictOpError{";
-  ss_ << ".sequence = " << static_cast<uint64_t>(sequence);
+  ss_ << ".sequence = " << static_cast<uint64_t>(sequence) << ", ";
+  ss_ << ".bad_value = " << static_cast<uint64_t>(bad_value) << ", ";
+  ss_ << ".minor_opcode = " << static_cast<uint64_t>(minor_opcode) << ", ";
+  ss_ << ".major_opcode = " << static_cast<uint64_t>(major_opcode);
   ss_ << "}";
   return ss_.str();
 }
@@ -123,6 +156,9 @@
   auto& buf = *buffer;
 
   auto& sequence = (*error_).sequence;
+  auto& bad_value = (*error_).bad_value;
+  auto& minor_opcode = (*error_).minor_opcode;
+  auto& major_opcode = (*error_).major_opcode;
 
   // response_type
   uint8_t response_type;
@@ -135,12 +171,24 @@
   // sequence
   Read(&sequence, &buf);
 
+  // bad_value
+  Read(&bad_value, &buf);
+
+  // minor_opcode
+  Read(&minor_opcode, &buf);
+
+  // major_opcode
+  Read(&major_opcode, &buf);
+
   DCHECK_LE(buf.offset, 32ul);
 }
 std::string Render::GlyphSetError::ToString() const {
   std::stringstream ss_;
   ss_ << "Render::GlyphSetError{";
-  ss_ << ".sequence = " << static_cast<uint64_t>(sequence);
+  ss_ << ".sequence = " << static_cast<uint64_t>(sequence) << ", ";
+  ss_ << ".bad_value = " << static_cast<uint64_t>(bad_value) << ", ";
+  ss_ << ".minor_opcode = " << static_cast<uint64_t>(minor_opcode) << ", ";
+  ss_ << ".major_opcode = " << static_cast<uint64_t>(major_opcode);
   ss_ << "}";
   return ss_.str();
 }
@@ -151,6 +199,9 @@
   auto& buf = *buffer;
 
   auto& sequence = (*error_).sequence;
+  auto& bad_value = (*error_).bad_value;
+  auto& minor_opcode = (*error_).minor_opcode;
+  auto& major_opcode = (*error_).major_opcode;
 
   // response_type
   uint8_t response_type;
@@ -163,12 +214,24 @@
   // sequence
   Read(&sequence, &buf);
 
+  // bad_value
+  Read(&bad_value, &buf);
+
+  // minor_opcode
+  Read(&minor_opcode, &buf);
+
+  // major_opcode
+  Read(&major_opcode, &buf);
+
   DCHECK_LE(buf.offset, 32ul);
 }
 std::string Render::GlyphError::ToString() const {
   std::stringstream ss_;
   ss_ << "Render::GlyphError{";
-  ss_ << ".sequence = " << static_cast<uint64_t>(sequence);
+  ss_ << ".sequence = " << static_cast<uint64_t>(sequence) << ", ";
+  ss_ << ".bad_value = " << static_cast<uint64_t>(bad_value) << ", ";
+  ss_ << ".minor_opcode = " << static_cast<uint64_t>(minor_opcode) << ", ";
+  ss_ << ".major_opcode = " << static_cast<uint64_t>(major_opcode);
   ss_ << "}";
   return ss_.str();
 }
@@ -179,6 +242,9 @@
   auto& buf = *buffer;
 
   auto& sequence = (*error_).sequence;
+  auto& bad_value = (*error_).bad_value;
+  auto& minor_opcode = (*error_).minor_opcode;
+  auto& major_opcode = (*error_).major_opcode;
 
   // response_type
   uint8_t response_type;
@@ -191,6 +257,15 @@
   // sequence
   Read(&sequence, &buf);
 
+  // bad_value
+  Read(&bad_value, &buf);
+
+  // minor_opcode
+  Read(&minor_opcode, &buf);
+
+  // major_opcode
+  Read(&major_opcode, &buf);
+
   DCHECK_LE(buf.offset, 32ul);
 }
 Future<Render::QueryVersionReply> Render::QueryVersion(
diff --git a/ui/gfx/x/generated_protos/render.h b/ui/gfx/x/generated_protos/render.h
index ea70403..cd1aba16 100644
--- a/ui/gfx/x/generated_protos/render.h
+++ b/ui/gfx/x/generated_protos/render.h
@@ -197,30 +197,45 @@
 
   struct PictFormatError : public x11::Error {
     uint16_t sequence{};
+    uint32_t bad_value{};
+    uint16_t minor_opcode{};
+    uint8_t major_opcode{};
 
     std::string ToString() const override;
   };
 
   struct PictureError : public x11::Error {
     uint16_t sequence{};
+    uint32_t bad_value{};
+    uint16_t minor_opcode{};
+    uint8_t major_opcode{};
 
     std::string ToString() const override;
   };
 
   struct PictOpError : public x11::Error {
     uint16_t sequence{};
+    uint32_t bad_value{};
+    uint16_t minor_opcode{};
+    uint8_t major_opcode{};
 
     std::string ToString() const override;
   };
 
   struct GlyphSetError : public x11::Error {
     uint16_t sequence{};
+    uint32_t bad_value{};
+    uint16_t minor_opcode{};
+    uint8_t major_opcode{};
 
     std::string ToString() const override;
   };
 
   struct GlyphError : public x11::Error {
     uint16_t sequence{};
+    uint32_t bad_value{};
+    uint16_t minor_opcode{};
+    uint8_t major_opcode{};
 
     std::string ToString() const override;
   };
diff --git a/ui/gfx/x/generated_protos/xf86vidmode.cc b/ui/gfx/x/generated_protos/xf86vidmode.cc
index 509cf8b..98fb120 100644
--- a/ui/gfx/x/generated_protos/xf86vidmode.cc
+++ b/ui/gfx/x/generated_protos/xf86vidmode.cc
@@ -57,7 +57,10 @@
 std::string XF86VidMode::BadClockError::ToString() const {
   std::stringstream ss_;
   ss_ << "XF86VidMode::BadClockError{";
-  ss_ << ".sequence = " << static_cast<uint64_t>(sequence);
+  ss_ << ".sequence = " << static_cast<uint64_t>(sequence) << ", ";
+  ss_ << ".bad_value = " << static_cast<uint64_t>(bad_value) << ", ";
+  ss_ << ".minor_opcode = " << static_cast<uint64_t>(minor_opcode) << ", ";
+  ss_ << ".major_opcode = " << static_cast<uint64_t>(major_opcode);
   ss_ << "}";
   return ss_.str();
 }
@@ -68,6 +71,9 @@
   auto& buf = *buffer;
 
   auto& sequence = (*error_).sequence;
+  auto& bad_value = (*error_).bad_value;
+  auto& minor_opcode = (*error_).minor_opcode;
+  auto& major_opcode = (*error_).major_opcode;
 
   // response_type
   uint8_t response_type;
@@ -80,12 +86,24 @@
   // sequence
   Read(&sequence, &buf);
 
+  // bad_value
+  Read(&bad_value, &buf);
+
+  // minor_opcode
+  Read(&minor_opcode, &buf);
+
+  // major_opcode
+  Read(&major_opcode, &buf);
+
   DCHECK_LE(buf.offset, 32ul);
 }
 std::string XF86VidMode::BadHTimingsError::ToString() const {
   std::stringstream ss_;
   ss_ << "XF86VidMode::BadHTimingsError{";
-  ss_ << ".sequence = " << static_cast<uint64_t>(sequence);
+  ss_ << ".sequence = " << static_cast<uint64_t>(sequence) << ", ";
+  ss_ << ".bad_value = " << static_cast<uint64_t>(bad_value) << ", ";
+  ss_ << ".minor_opcode = " << static_cast<uint64_t>(minor_opcode) << ", ";
+  ss_ << ".major_opcode = " << static_cast<uint64_t>(major_opcode);
   ss_ << "}";
   return ss_.str();
 }
@@ -97,6 +115,9 @@
   auto& buf = *buffer;
 
   auto& sequence = (*error_).sequence;
+  auto& bad_value = (*error_).bad_value;
+  auto& minor_opcode = (*error_).minor_opcode;
+  auto& major_opcode = (*error_).major_opcode;
 
   // response_type
   uint8_t response_type;
@@ -109,12 +130,24 @@
   // sequence
   Read(&sequence, &buf);
 
+  // bad_value
+  Read(&bad_value, &buf);
+
+  // minor_opcode
+  Read(&minor_opcode, &buf);
+
+  // major_opcode
+  Read(&major_opcode, &buf);
+
   DCHECK_LE(buf.offset, 32ul);
 }
 std::string XF86VidMode::BadVTimingsError::ToString() const {
   std::stringstream ss_;
   ss_ << "XF86VidMode::BadVTimingsError{";
-  ss_ << ".sequence = " << static_cast<uint64_t>(sequence);
+  ss_ << ".sequence = " << static_cast<uint64_t>(sequence) << ", ";
+  ss_ << ".bad_value = " << static_cast<uint64_t>(bad_value) << ", ";
+  ss_ << ".minor_opcode = " << static_cast<uint64_t>(minor_opcode) << ", ";
+  ss_ << ".major_opcode = " << static_cast<uint64_t>(major_opcode);
   ss_ << "}";
   return ss_.str();
 }
@@ -126,6 +159,9 @@
   auto& buf = *buffer;
 
   auto& sequence = (*error_).sequence;
+  auto& bad_value = (*error_).bad_value;
+  auto& minor_opcode = (*error_).minor_opcode;
+  auto& major_opcode = (*error_).major_opcode;
 
   // response_type
   uint8_t response_type;
@@ -138,12 +174,24 @@
   // sequence
   Read(&sequence, &buf);
 
+  // bad_value
+  Read(&bad_value, &buf);
+
+  // minor_opcode
+  Read(&minor_opcode, &buf);
+
+  // major_opcode
+  Read(&major_opcode, &buf);
+
   DCHECK_LE(buf.offset, 32ul);
 }
 std::string XF86VidMode::ModeUnsuitableError::ToString() const {
   std::stringstream ss_;
   ss_ << "XF86VidMode::ModeUnsuitableError{";
-  ss_ << ".sequence = " << static_cast<uint64_t>(sequence);
+  ss_ << ".sequence = " << static_cast<uint64_t>(sequence) << ", ";
+  ss_ << ".bad_value = " << static_cast<uint64_t>(bad_value) << ", ";
+  ss_ << ".minor_opcode = " << static_cast<uint64_t>(minor_opcode) << ", ";
+  ss_ << ".major_opcode = " << static_cast<uint64_t>(major_opcode);
   ss_ << "}";
   return ss_.str();
 }
@@ -155,6 +203,9 @@
   auto& buf = *buffer;
 
   auto& sequence = (*error_).sequence;
+  auto& bad_value = (*error_).bad_value;
+  auto& minor_opcode = (*error_).minor_opcode;
+  auto& major_opcode = (*error_).major_opcode;
 
   // response_type
   uint8_t response_type;
@@ -167,12 +218,24 @@
   // sequence
   Read(&sequence, &buf);
 
+  // bad_value
+  Read(&bad_value, &buf);
+
+  // minor_opcode
+  Read(&minor_opcode, &buf);
+
+  // major_opcode
+  Read(&major_opcode, &buf);
+
   DCHECK_LE(buf.offset, 32ul);
 }
 std::string XF86VidMode::ExtensionDisabledError::ToString() const {
   std::stringstream ss_;
   ss_ << "XF86VidMode::ExtensionDisabledError{";
-  ss_ << ".sequence = " << static_cast<uint64_t>(sequence);
+  ss_ << ".sequence = " << static_cast<uint64_t>(sequence) << ", ";
+  ss_ << ".bad_value = " << static_cast<uint64_t>(bad_value) << ", ";
+  ss_ << ".minor_opcode = " << static_cast<uint64_t>(minor_opcode) << ", ";
+  ss_ << ".major_opcode = " << static_cast<uint64_t>(major_opcode);
   ss_ << "}";
   return ss_.str();
 }
@@ -184,6 +247,9 @@
   auto& buf = *buffer;
 
   auto& sequence = (*error_).sequence;
+  auto& bad_value = (*error_).bad_value;
+  auto& minor_opcode = (*error_).minor_opcode;
+  auto& major_opcode = (*error_).major_opcode;
 
   // response_type
   uint8_t response_type;
@@ -196,12 +262,24 @@
   // sequence
   Read(&sequence, &buf);
 
+  // bad_value
+  Read(&bad_value, &buf);
+
+  // minor_opcode
+  Read(&minor_opcode, &buf);
+
+  // major_opcode
+  Read(&major_opcode, &buf);
+
   DCHECK_LE(buf.offset, 32ul);
 }
 std::string XF86VidMode::ClientNotLocalError::ToString() const {
   std::stringstream ss_;
   ss_ << "XF86VidMode::ClientNotLocalError{";
-  ss_ << ".sequence = " << static_cast<uint64_t>(sequence);
+  ss_ << ".sequence = " << static_cast<uint64_t>(sequence) << ", ";
+  ss_ << ".bad_value = " << static_cast<uint64_t>(bad_value) << ", ";
+  ss_ << ".minor_opcode = " << static_cast<uint64_t>(minor_opcode) << ", ";
+  ss_ << ".major_opcode = " << static_cast<uint64_t>(major_opcode);
   ss_ << "}";
   return ss_.str();
 }
@@ -213,6 +291,9 @@
   auto& buf = *buffer;
 
   auto& sequence = (*error_).sequence;
+  auto& bad_value = (*error_).bad_value;
+  auto& minor_opcode = (*error_).minor_opcode;
+  auto& major_opcode = (*error_).major_opcode;
 
   // response_type
   uint8_t response_type;
@@ -225,12 +306,24 @@
   // sequence
   Read(&sequence, &buf);
 
+  // bad_value
+  Read(&bad_value, &buf);
+
+  // minor_opcode
+  Read(&minor_opcode, &buf);
+
+  // major_opcode
+  Read(&major_opcode, &buf);
+
   DCHECK_LE(buf.offset, 32ul);
 }
 std::string XF86VidMode::ZoomLockedError::ToString() const {
   std::stringstream ss_;
   ss_ << "XF86VidMode::ZoomLockedError{";
-  ss_ << ".sequence = " << static_cast<uint64_t>(sequence);
+  ss_ << ".sequence = " << static_cast<uint64_t>(sequence) << ", ";
+  ss_ << ".bad_value = " << static_cast<uint64_t>(bad_value) << ", ";
+  ss_ << ".minor_opcode = " << static_cast<uint64_t>(minor_opcode) << ", ";
+  ss_ << ".major_opcode = " << static_cast<uint64_t>(major_opcode);
   ss_ << "}";
   return ss_.str();
 }
@@ -242,6 +335,9 @@
   auto& buf = *buffer;
 
   auto& sequence = (*error_).sequence;
+  auto& bad_value = (*error_).bad_value;
+  auto& minor_opcode = (*error_).minor_opcode;
+  auto& major_opcode = (*error_).major_opcode;
 
   // response_type
   uint8_t response_type;
@@ -254,6 +350,15 @@
   // sequence
   Read(&sequence, &buf);
 
+  // bad_value
+  Read(&bad_value, &buf);
+
+  // minor_opcode
+  Read(&minor_opcode, &buf);
+
+  // major_opcode
+  Read(&major_opcode, &buf);
+
   DCHECK_LE(buf.offset, 32ul);
 }
 Future<XF86VidMode::QueryVersionReply> XF86VidMode::QueryVersion(
diff --git a/ui/gfx/x/generated_protos/xf86vidmode.h b/ui/gfx/x/generated_protos/xf86vidmode.h
index f49b96c..b85bfc4 100644
--- a/ui/gfx/x/generated_protos/xf86vidmode.h
+++ b/ui/gfx/x/generated_protos/xf86vidmode.h
@@ -135,42 +135,63 @@
 
   struct BadClockError : public x11::Error {
     uint16_t sequence{};
+    uint32_t bad_value{};
+    uint16_t minor_opcode{};
+    uint8_t major_opcode{};
 
     std::string ToString() const override;
   };
 
   struct BadHTimingsError : public x11::Error {
     uint16_t sequence{};
+    uint32_t bad_value{};
+    uint16_t minor_opcode{};
+    uint8_t major_opcode{};
 
     std::string ToString() const override;
   };
 
   struct BadVTimingsError : public x11::Error {
     uint16_t sequence{};
+    uint32_t bad_value{};
+    uint16_t minor_opcode{};
+    uint8_t major_opcode{};
 
     std::string ToString() const override;
   };
 
   struct ModeUnsuitableError : public x11::Error {
     uint16_t sequence{};
+    uint32_t bad_value{};
+    uint16_t minor_opcode{};
+    uint8_t major_opcode{};
 
     std::string ToString() const override;
   };
 
   struct ExtensionDisabledError : public x11::Error {
     uint16_t sequence{};
+    uint32_t bad_value{};
+    uint16_t minor_opcode{};
+    uint8_t major_opcode{};
 
     std::string ToString() const override;
   };
 
   struct ClientNotLocalError : public x11::Error {
     uint16_t sequence{};
+    uint32_t bad_value{};
+    uint16_t minor_opcode{};
+    uint8_t major_opcode{};
 
     std::string ToString() const override;
   };
 
   struct ZoomLockedError : public x11::Error {
     uint16_t sequence{};
+    uint32_t bad_value{};
+    uint16_t minor_opcode{};
+    uint8_t major_opcode{};
 
     std::string ToString() const override;
   };
diff --git a/ui/gfx/x/generated_protos/xfixes.cc b/ui/gfx/x/generated_protos/xfixes.cc
index e3b44e6..f0ec6022 100644
--- a/ui/gfx/x/generated_protos/xfixes.cc
+++ b/ui/gfx/x/generated_protos/xfixes.cc
@@ -147,7 +147,10 @@
 std::string XFixes::BadRegionError::ToString() const {
   std::stringstream ss_;
   ss_ << "XFixes::BadRegionError{";
-  ss_ << ".sequence = " << static_cast<uint64_t>(sequence);
+  ss_ << ".sequence = " << static_cast<uint64_t>(sequence) << ", ";
+  ss_ << ".bad_value = " << static_cast<uint64_t>(bad_value) << ", ";
+  ss_ << ".minor_opcode = " << static_cast<uint64_t>(minor_opcode) << ", ";
+  ss_ << ".major_opcode = " << static_cast<uint64_t>(major_opcode);
   ss_ << "}";
   return ss_.str();
 }
@@ -158,6 +161,9 @@
   auto& buf = *buffer;
 
   auto& sequence = (*error_).sequence;
+  auto& bad_value = (*error_).bad_value;
+  auto& minor_opcode = (*error_).minor_opcode;
+  auto& major_opcode = (*error_).major_opcode;
 
   // response_type
   uint8_t response_type;
@@ -170,6 +176,15 @@
   // sequence
   Read(&sequence, &buf);
 
+  // bad_value
+  Read(&bad_value, &buf);
+
+  // minor_opcode
+  Read(&minor_opcode, &buf);
+
+  // major_opcode
+  Read(&major_opcode, &buf);
+
   DCHECK_LE(buf.offset, 32ul);
 }
 Future<XFixes::QueryVersionReply> XFixes::QueryVersion(
@@ -1985,4 +2000,110 @@
       XFixes::DeletePointerBarrierRequest{barrier});
 }
 
+Future<void> XFixes::SetClientDisconnectMode(
+    const XFixes::SetClientDisconnectModeRequest& request) {
+  if (!connection_->Ready() || !present())
+    return {};
+
+  WriteBuffer buf;
+
+  auto& disconnect_mode = request.disconnect_mode;
+
+  // major_opcode
+  uint8_t major_opcode = info_.major_opcode;
+  buf.Write(&major_opcode);
+
+  // minor_opcode
+  uint8_t minor_opcode = 33;
+  buf.Write(&minor_opcode);
+
+  // length
+  // Caller fills in length for writes.
+  Pad(&buf, sizeof(uint16_t));
+
+  // disconnect_mode
+  uint32_t tmp10;
+  tmp10 = static_cast<uint32_t>(disconnect_mode);
+  buf.Write(&tmp10);
+
+  Align(&buf, 4);
+
+  return connection_->SendRequest<void>(&buf, "XFixes::SetClientDisconnectMode",
+                                        false);
+}
+
+Future<void> XFixes::SetClientDisconnectMode(
+    const ClientDisconnectFlags& disconnect_mode) {
+  return XFixes::SetClientDisconnectMode(
+      XFixes::SetClientDisconnectModeRequest{disconnect_mode});
+}
+
+Future<XFixes::GetClientDisconnectModeReply> XFixes::GetClientDisconnectMode(
+    const XFixes::GetClientDisconnectModeRequest& request) {
+  if (!connection_->Ready() || !present())
+    return {};
+
+  WriteBuffer buf;
+
+  // major_opcode
+  uint8_t major_opcode = info_.major_opcode;
+  buf.Write(&major_opcode);
+
+  // minor_opcode
+  uint8_t minor_opcode = 34;
+  buf.Write(&minor_opcode);
+
+  // length
+  // Caller fills in length for writes.
+  Pad(&buf, sizeof(uint16_t));
+
+  Align(&buf, 4);
+
+  return connection_->SendRequest<XFixes::GetClientDisconnectModeReply>(
+      &buf, "XFixes::GetClientDisconnectMode", false);
+}
+
+Future<XFixes::GetClientDisconnectModeReply> XFixes::GetClientDisconnectMode() {
+  return XFixes::GetClientDisconnectMode(
+      XFixes::GetClientDisconnectModeRequest{});
+}
+
+template <>
+COMPONENT_EXPORT(X11)
+std::unique_ptr<XFixes::GetClientDisconnectModeReply> detail::ReadReply<
+    XFixes::GetClientDisconnectModeReply>(ReadBuffer* buffer) {
+  auto& buf = *buffer;
+  auto reply = std::make_unique<XFixes::GetClientDisconnectModeReply>();
+
+  auto& sequence = (*reply).sequence;
+  auto& disconnect_mode = (*reply).disconnect_mode;
+
+  // response_type
+  uint8_t response_type;
+  Read(&response_type, &buf);
+
+  // pad0
+  Pad(&buf, 1);
+
+  // sequence
+  Read(&sequence, &buf);
+
+  // length
+  uint32_t length;
+  Read(&length, &buf);
+
+  // disconnect_mode
+  uint32_t tmp11;
+  Read(&tmp11, &buf);
+  disconnect_mode = static_cast<XFixes::ClientDisconnectFlags>(tmp11);
+
+  // pad1
+  Pad(&buf, 20);
+
+  Align(&buf, 4);
+  DCHECK_EQ(buf.offset < 32 ? 0 : buf.offset - 32, 4 * length);
+
+  return reply;
+}
+
 }  // namespace x11
diff --git a/ui/gfx/x/generated_protos/xfixes.h b/ui/gfx/x/generated_protos/xfixes.h
index 75385d0..b0aa4b9 100644
--- a/ui/gfx/x/generated_protos/xfixes.h
+++ b/ui/gfx/x/generated_protos/xfixes.h
@@ -70,7 +70,7 @@
 
 class COMPONENT_EXPORT(X11) XFixes {
  public:
-  static constexpr unsigned major_version = 5;
+  static constexpr unsigned major_version = 6;
   static constexpr unsigned minor_version = 0;
 
   XFixes(Connection* connection, const x11::QueryExtensionReply& info);
@@ -130,6 +130,11 @@
     NegativeY = 1 << 3,
   };
 
+  enum class ClientDisconnectFlags : int {
+    Default = 0,
+    Terminate = 1 << 0,
+  };
+
   struct SelectionNotifyEvent {
     static constexpr int type_id = 18;
     static constexpr uint8_t opcode = 0;
@@ -159,6 +164,9 @@
 
   struct BadRegionError : public x11::Error {
     uint16_t sequence{};
+    uint32_t bad_value{};
+    uint16_t minor_opcode{};
+    uint8_t major_opcode{};
 
     std::string ToString() const override;
   };
@@ -641,6 +649,33 @@
 
   Future<void> DeletePointerBarrier(const Barrier& barrier = {});
 
+  struct SetClientDisconnectModeRequest {
+    ClientDisconnectFlags disconnect_mode{};
+  };
+
+  using SetClientDisconnectModeResponse = Response<void>;
+
+  Future<void> SetClientDisconnectMode(
+      const SetClientDisconnectModeRequest& request);
+
+  Future<void> SetClientDisconnectMode(
+      const ClientDisconnectFlags& disconnect_mode = {});
+
+  struct GetClientDisconnectModeRequest {};
+
+  struct GetClientDisconnectModeReply {
+    uint16_t sequence{};
+    ClientDisconnectFlags disconnect_mode{};
+  };
+
+  using GetClientDisconnectModeResponse =
+      Response<GetClientDisconnectModeReply>;
+
+  Future<GetClientDisconnectModeReply> GetClientDisconnectMode(
+      const GetClientDisconnectModeRequest& request);
+
+  Future<GetClientDisconnectModeReply> GetClientDisconnectMode();
+
  private:
   Connection* const connection_;
   x11::QueryExtensionReply info_{};
@@ -790,4 +825,20 @@
                                                      static_cast<T>(r));
 }
 
+inline constexpr x11::XFixes::ClientDisconnectFlags operator|(
+    x11::XFixes::ClientDisconnectFlags l,
+    x11::XFixes::ClientDisconnectFlags r) {
+  using T = std::underlying_type_t<x11::XFixes::ClientDisconnectFlags>;
+  return static_cast<x11::XFixes::ClientDisconnectFlags>(static_cast<T>(l) |
+                                                         static_cast<T>(r));
+}
+
+inline constexpr x11::XFixes::ClientDisconnectFlags operator&(
+    x11::XFixes::ClientDisconnectFlags l,
+    x11::XFixes::ClientDisconnectFlags r) {
+  using T = std::underlying_type_t<x11::XFixes::ClientDisconnectFlags>;
+  return static_cast<x11::XFixes::ClientDisconnectFlags>(static_cast<T>(l) &
+                                                         static_cast<T>(r));
+}
+
 #endif  // UI_GFX_X_GENERATED_PROTOS_XFIXES_H_
diff --git a/ui/gfx/x/generated_protos/xinput.cc b/ui/gfx/x/generated_protos/xinput.cc
index a89a198..2c4abd6 100644
--- a/ui/gfx/x/generated_protos/xinput.cc
+++ b/ui/gfx/x/generated_protos/xinput.cc
@@ -734,6 +734,16 @@
         // num_touches
         Read(&num_touches, &buf);
       }
+      if (CaseEq(data_expr, Input::DeviceClassType::Gesture)) {
+        data.gesture.emplace();
+        auto& num_touches = (*data.gesture).num_touches;
+
+        // num_touches
+        Read(&num_touches, &buf);
+
+        // pad2
+        Pad(&buf, 1);
+      }
     }
   }
 
@@ -1485,10 +1495,305 @@
   DCHECK_EQ(buf.offset, 32 + 4 * length);
 }
 
+template <>
+COMPONENT_EXPORT(X11)
+void ReadEvent<Input::GesturePinchEvent>(Input::GesturePinchEvent* event_,
+                                         ReadBuffer* buffer) {
+  auto& buf = *buffer;
+
+  auto& sequence = (*event_).sequence;
+  auto& deviceid = (*event_).deviceid;
+  auto& time = (*event_).time;
+  auto& detail = (*event_).detail;
+  auto& root = (*event_).root;
+  auto& event = (*event_).event;
+  auto& child = (*event_).child;
+  auto& root_x = (*event_).root_x;
+  auto& root_y = (*event_).root_y;
+  auto& event_x = (*event_).event_x;
+  auto& event_y = (*event_).event_y;
+  auto& delta_x = (*event_).delta_x;
+  auto& delta_y = (*event_).delta_y;
+  auto& delta_unaccel_x = (*event_).delta_unaccel_x;
+  auto& delta_unaccel_y = (*event_).delta_unaccel_y;
+  auto& scale = (*event_).scale;
+  auto& delta_angle = (*event_).delta_angle;
+  auto& sourceid = (*event_).sourceid;
+  auto& mods = (*event_).mods;
+  auto& group = (*event_).group;
+  auto& flags = (*event_).flags;
+
+  // response_type
+  uint8_t response_type;
+  Read(&response_type, &buf);
+
+  // extension
+  uint8_t extension;
+  Read(&extension, &buf);
+
+  // sequence
+  Read(&sequence, &buf);
+
+  // length
+  uint32_t length;
+  Read(&length, &buf);
+
+  // event_type
+  uint16_t event_type;
+  Read(&event_type, &buf);
+
+  // deviceid
+  Read(&deviceid, &buf);
+
+  // time
+  Read(&time, &buf);
+
+  // detail
+  Read(&detail, &buf);
+
+  // root
+  Read(&root, &buf);
+
+  // event
+  Read(&event, &buf);
+
+  // child
+  Read(&child, &buf);
+
+  // root_x
+  Read(&root_x, &buf);
+
+  // root_y
+  Read(&root_y, &buf);
+
+  // event_x
+  Read(&event_x, &buf);
+
+  // event_y
+  Read(&event_y, &buf);
+
+  // delta_x
+  Read(&delta_x, &buf);
+
+  // delta_y
+  Read(&delta_y, &buf);
+
+  // delta_unaccel_x
+  Read(&delta_unaccel_x, &buf);
+
+  // delta_unaccel_y
+  Read(&delta_unaccel_y, &buf);
+
+  // scale
+  Read(&scale, &buf);
+
+  // delta_angle
+  Read(&delta_angle, &buf);
+
+  // sourceid
+  Read(&sourceid, &buf);
+
+  // pad0
+  Pad(&buf, 2);
+
+  // mods
+  {
+    auto& base = mods.base;
+    auto& latched = mods.latched;
+    auto& locked = mods.locked;
+    auto& effective = mods.effective;
+
+    // base
+    Read(&base, &buf);
+
+    // latched
+    Read(&latched, &buf);
+
+    // locked
+    Read(&locked, &buf);
+
+    // effective
+    Read(&effective, &buf);
+  }
+
+  // group
+  {
+    auto& base = group.base;
+    auto& latched = group.latched;
+    auto& locked = group.locked;
+    auto& effective = group.effective;
+
+    // base
+    Read(&base, &buf);
+
+    // latched
+    Read(&latched, &buf);
+
+    // locked
+    Read(&locked, &buf);
+
+    // effective
+    Read(&effective, &buf);
+  }
+
+  // flags
+  uint32_t tmp27;
+  Read(&tmp27, &buf);
+  flags = static_cast<Input::GesturePinchEventFlags>(tmp27);
+
+  Align(&buf, 4);
+  DCHECK_EQ(buf.offset, 32 + 4 * length);
+}
+
+template <>
+COMPONENT_EXPORT(X11)
+void ReadEvent<Input::GestureSwipeEvent>(Input::GestureSwipeEvent* event_,
+                                         ReadBuffer* buffer) {
+  auto& buf = *buffer;
+
+  auto& sequence = (*event_).sequence;
+  auto& deviceid = (*event_).deviceid;
+  auto& time = (*event_).time;
+  auto& detail = (*event_).detail;
+  auto& root = (*event_).root;
+  auto& event = (*event_).event;
+  auto& child = (*event_).child;
+  auto& root_x = (*event_).root_x;
+  auto& root_y = (*event_).root_y;
+  auto& event_x = (*event_).event_x;
+  auto& event_y = (*event_).event_y;
+  auto& delta_x = (*event_).delta_x;
+  auto& delta_y = (*event_).delta_y;
+  auto& delta_unaccel_x = (*event_).delta_unaccel_x;
+  auto& delta_unaccel_y = (*event_).delta_unaccel_y;
+  auto& sourceid = (*event_).sourceid;
+  auto& mods = (*event_).mods;
+  auto& group = (*event_).group;
+  auto& flags = (*event_).flags;
+
+  // response_type
+  uint8_t response_type;
+  Read(&response_type, &buf);
+
+  // extension
+  uint8_t extension;
+  Read(&extension, &buf);
+
+  // sequence
+  Read(&sequence, &buf);
+
+  // length
+  uint32_t length;
+  Read(&length, &buf);
+
+  // event_type
+  uint16_t event_type;
+  Read(&event_type, &buf);
+
+  // deviceid
+  Read(&deviceid, &buf);
+
+  // time
+  Read(&time, &buf);
+
+  // detail
+  Read(&detail, &buf);
+
+  // root
+  Read(&root, &buf);
+
+  // event
+  Read(&event, &buf);
+
+  // child
+  Read(&child, &buf);
+
+  // root_x
+  Read(&root_x, &buf);
+
+  // root_y
+  Read(&root_y, &buf);
+
+  // event_x
+  Read(&event_x, &buf);
+
+  // event_y
+  Read(&event_y, &buf);
+
+  // delta_x
+  Read(&delta_x, &buf);
+
+  // delta_y
+  Read(&delta_y, &buf);
+
+  // delta_unaccel_x
+  Read(&delta_unaccel_x, &buf);
+
+  // delta_unaccel_y
+  Read(&delta_unaccel_y, &buf);
+
+  // sourceid
+  Read(&sourceid, &buf);
+
+  // pad0
+  Pad(&buf, 2);
+
+  // mods
+  {
+    auto& base = mods.base;
+    auto& latched = mods.latched;
+    auto& locked = mods.locked;
+    auto& effective = mods.effective;
+
+    // base
+    Read(&base, &buf);
+
+    // latched
+    Read(&latched, &buf);
+
+    // locked
+    Read(&locked, &buf);
+
+    // effective
+    Read(&effective, &buf);
+  }
+
+  // group
+  {
+    auto& base = group.base;
+    auto& latched = group.latched;
+    auto& locked = group.locked;
+    auto& effective = group.effective;
+
+    // base
+    Read(&base, &buf);
+
+    // latched
+    Read(&latched, &buf);
+
+    // locked
+    Read(&locked, &buf);
+
+    // effective
+    Read(&effective, &buf);
+  }
+
+  // flags
+  uint32_t tmp28;
+  Read(&tmp28, &buf);
+  flags = static_cast<Input::GestureSwipeEventFlags>(tmp28);
+
+  Align(&buf, 4);
+  DCHECK_EQ(buf.offset, 32 + 4 * length);
+}
+
 std::string Input::DeviceError::ToString() const {
   std::stringstream ss_;
   ss_ << "Input::DeviceError{";
-  ss_ << ".sequence = " << static_cast<uint64_t>(sequence);
+  ss_ << ".sequence = " << static_cast<uint64_t>(sequence) << ", ";
+  ss_ << ".bad_value = " << static_cast<uint64_t>(bad_value) << ", ";
+  ss_ << ".minor_opcode = " << static_cast<uint64_t>(minor_opcode) << ", ";
+  ss_ << ".major_opcode = " << static_cast<uint64_t>(major_opcode);
   ss_ << "}";
   return ss_.str();
 }
@@ -1499,6 +1804,9 @@
   auto& buf = *buffer;
 
   auto& sequence = (*error_).sequence;
+  auto& bad_value = (*error_).bad_value;
+  auto& minor_opcode = (*error_).minor_opcode;
+  auto& major_opcode = (*error_).major_opcode;
 
   // response_type
   uint8_t response_type;
@@ -1511,12 +1819,24 @@
   // sequence
   Read(&sequence, &buf);
 
+  // bad_value
+  Read(&bad_value, &buf);
+
+  // minor_opcode
+  Read(&minor_opcode, &buf);
+
+  // major_opcode
+  Read(&major_opcode, &buf);
+
   DCHECK_LE(buf.offset, 32ul);
 }
 std::string Input::EventError::ToString() const {
   std::stringstream ss_;
   ss_ << "Input::EventError{";
-  ss_ << ".sequence = " << static_cast<uint64_t>(sequence);
+  ss_ << ".sequence = " << static_cast<uint64_t>(sequence) << ", ";
+  ss_ << ".bad_value = " << static_cast<uint64_t>(bad_value) << ", ";
+  ss_ << ".minor_opcode = " << static_cast<uint64_t>(minor_opcode) << ", ";
+  ss_ << ".major_opcode = " << static_cast<uint64_t>(major_opcode);
   ss_ << "}";
   return ss_.str();
 }
@@ -1527,6 +1847,9 @@
   auto& buf = *buffer;
 
   auto& sequence = (*error_).sequence;
+  auto& bad_value = (*error_).bad_value;
+  auto& minor_opcode = (*error_).minor_opcode;
+  auto& major_opcode = (*error_).major_opcode;
 
   // response_type
   uint8_t response_type;
@@ -1539,12 +1862,24 @@
   // sequence
   Read(&sequence, &buf);
 
+  // bad_value
+  Read(&bad_value, &buf);
+
+  // minor_opcode
+  Read(&minor_opcode, &buf);
+
+  // major_opcode
+  Read(&major_opcode, &buf);
+
   DCHECK_LE(buf.offset, 32ul);
 }
 std::string Input::ModeError::ToString() const {
   std::stringstream ss_;
   ss_ << "Input::ModeError{";
-  ss_ << ".sequence = " << static_cast<uint64_t>(sequence);
+  ss_ << ".sequence = " << static_cast<uint64_t>(sequence) << ", ";
+  ss_ << ".bad_value = " << static_cast<uint64_t>(bad_value) << ", ";
+  ss_ << ".minor_opcode = " << static_cast<uint64_t>(minor_opcode) << ", ";
+  ss_ << ".major_opcode = " << static_cast<uint64_t>(major_opcode);
   ss_ << "}";
   return ss_.str();
 }
@@ -1554,6 +1889,9 @@
   auto& buf = *buffer;
 
   auto& sequence = (*error_).sequence;
+  auto& bad_value = (*error_).bad_value;
+  auto& minor_opcode = (*error_).minor_opcode;
+  auto& major_opcode = (*error_).major_opcode;
 
   // response_type
   uint8_t response_type;
@@ -1566,12 +1904,24 @@
   // sequence
   Read(&sequence, &buf);
 
+  // bad_value
+  Read(&bad_value, &buf);
+
+  // minor_opcode
+  Read(&minor_opcode, &buf);
+
+  // major_opcode
+  Read(&major_opcode, &buf);
+
   DCHECK_LE(buf.offset, 32ul);
 }
 std::string Input::DeviceBusyError::ToString() const {
   std::stringstream ss_;
   ss_ << "Input::DeviceBusyError{";
-  ss_ << ".sequence = " << static_cast<uint64_t>(sequence);
+  ss_ << ".sequence = " << static_cast<uint64_t>(sequence) << ", ";
+  ss_ << ".bad_value = " << static_cast<uint64_t>(bad_value) << ", ";
+  ss_ << ".minor_opcode = " << static_cast<uint64_t>(minor_opcode) << ", ";
+  ss_ << ".major_opcode = " << static_cast<uint64_t>(major_opcode);
   ss_ << "}";
   return ss_.str();
 }
@@ -1582,6 +1932,9 @@
   auto& buf = *buffer;
 
   auto& sequence = (*error_).sequence;
+  auto& bad_value = (*error_).bad_value;
+  auto& minor_opcode = (*error_).minor_opcode;
+  auto& major_opcode = (*error_).major_opcode;
 
   // response_type
   uint8_t response_type;
@@ -1594,12 +1947,24 @@
   // sequence
   Read(&sequence, &buf);
 
+  // bad_value
+  Read(&bad_value, &buf);
+
+  // minor_opcode
+  Read(&minor_opcode, &buf);
+
+  // major_opcode
+  Read(&major_opcode, &buf);
+
   DCHECK_LE(buf.offset, 32ul);
 }
 std::string Input::ClassError::ToString() const {
   std::stringstream ss_;
   ss_ << "Input::ClassError{";
-  ss_ << ".sequence = " << static_cast<uint64_t>(sequence);
+  ss_ << ".sequence = " << static_cast<uint64_t>(sequence) << ", ";
+  ss_ << ".bad_value = " << static_cast<uint64_t>(bad_value) << ", ";
+  ss_ << ".minor_opcode = " << static_cast<uint64_t>(minor_opcode) << ", ";
+  ss_ << ".major_opcode = " << static_cast<uint64_t>(major_opcode);
   ss_ << "}";
   return ss_.str();
 }
@@ -1610,6 +1975,9 @@
   auto& buf = *buffer;
 
   auto& sequence = (*error_).sequence;
+  auto& bad_value = (*error_).bad_value;
+  auto& minor_opcode = (*error_).minor_opcode;
+  auto& major_opcode = (*error_).major_opcode;
 
   // response_type
   uint8_t response_type;
@@ -1622,6 +1990,15 @@
   // sequence
   Read(&sequence, &buf);
 
+  // bad_value
+  Read(&bad_value, &buf);
+
+  // minor_opcode
+  Read(&minor_opcode, &buf);
+
+  // major_opcode
+  Read(&major_opcode, &buf);
+
   DCHECK_LE(buf.offset, 32ul);
 }
 Future<Input::GetExtensionVersionReply> Input::GetExtensionVersion(
@@ -1801,9 +2178,9 @@
       Read(&num_class_info, &buf);
 
       // device_use
-      uint8_t tmp27;
-      Read(&tmp27, &buf);
-      device_use = static_cast<Input::DeviceUse>(tmp27);
+      uint8_t tmp29;
+      Read(&tmp29, &buf);
+      device_use = static_cast<Input::DeviceUse>(tmp29);
 
       // pad0
       Pad(&buf, 1);
@@ -1811,7 +2188,7 @@
   }
 
   // infos
-  auto sum28_ = SumOf(
+  auto sum30_ = SumOf(
       [](auto& listelem_ref) {
         auto& device_type = listelem_ref.device_type;
         auto& device_id = listelem_ref.device_id;
@@ -1821,7 +2198,7 @@
         return num_class_info;
       },
       devices);
-  infos.resize(sum28_);
+  infos.resize(sum30_);
   for (auto& infos_elem : infos) {
     // infos_elem
     {
@@ -1830,9 +2207,9 @@
       auto& info = infos_elem;
 
       // class_id
-      uint8_t tmp29;
-      Read(&tmp29, &buf);
-      class_id = static_cast<Input::InputClass>(tmp29);
+      uint8_t tmp31;
+      Read(&tmp31, &buf);
+      class_id = static_cast<Input::InputClass>(tmp31);
 
       // len
       Read(&len, &buf);
@@ -1875,9 +2252,9 @@
         Read(&axes_len, &buf);
 
         // mode
-        uint8_t tmp30;
-        Read(&tmp30, &buf);
-        mode = static_cast<Input::ValuatorMode>(tmp30);
+        uint8_t tmp32;
+        Read(&tmp32, &buf);
+        mode = static_cast<Input::ValuatorMode>(tmp32);
 
         // motion_size
         Read(&motion_size, &buf);
@@ -2013,9 +2390,9 @@
       auto& event_type_base = class_info_elem.event_type_base;
 
       // class_id
-      uint8_t tmp31;
-      Read(&tmp31, &buf);
-      class_id = static_cast<Input::InputClass>(tmp31);
+      uint8_t tmp33;
+      Read(&tmp33, &buf);
+      class_id = static_cast<Input::InputClass>(tmp33);
 
       // event_type_base
       Read(&event_type_base, &buf);
@@ -2092,9 +2469,9 @@
   buf.Write(&device_id);
 
   // mode
-  uint8_t tmp32;
-  tmp32 = static_cast<uint8_t>(mode);
-  buf.Write(&tmp32);
+  uint8_t tmp34;
+  tmp34 = static_cast<uint8_t>(mode);
+  buf.Write(&tmp34);
 
   // pad0
   Pad(&buf, 2);
@@ -2137,9 +2514,9 @@
   Read(&length, &buf);
 
   // status
-  uint8_t tmp33;
-  Read(&tmp33, &buf);
-  status = static_cast<GrabStatus>(tmp33);
+  uint8_t tmp35;
+  Read(&tmp35, &buf);
+  status = static_cast<GrabStatus>(tmp35);
 
   // pad0
   Pad(&buf, 23);
@@ -2333,9 +2710,9 @@
   buf.Write(&num_classes);
 
   // mode
-  uint8_t tmp34;
-  tmp34 = static_cast<uint8_t>(mode);
-  buf.Write(&tmp34);
+  uint8_t tmp36;
+  tmp36 = static_cast<uint8_t>(mode);
+  buf.Write(&tmp36);
 
   // pad0
   Pad(&buf, 1);
@@ -2529,9 +2906,9 @@
   Read(&num_axes, &buf);
 
   // device_mode
-  uint8_t tmp35;
-  Read(&tmp35, &buf);
-  device_mode = static_cast<Input::ValuatorMode>(tmp35);
+  uint8_t tmp37;
+  Read(&tmp37, &buf);
+  device_mode = static_cast<Input::ValuatorMode>(tmp37);
 
   // pad0
   Pad(&buf, 18);
@@ -2628,9 +3005,9 @@
   Read(&length, &buf);
 
   // status
-  uint8_t tmp36;
-  Read(&tmp36, &buf);
-  status = static_cast<GrabStatus>(tmp36);
+  uint8_t tmp38;
+  Read(&tmp38, &buf);
+  status = static_cast<GrabStatus>(tmp38);
 
   // pad0
   Pad(&buf, 23);
@@ -2716,9 +3093,9 @@
   Read(&length, &buf);
 
   // status
-  uint8_t tmp37;
-  Read(&tmp37, &buf);
-  status = static_cast<GrabStatus>(tmp37);
+  uint8_t tmp39;
+  Read(&tmp39, &buf);
+  status = static_cast<GrabStatus>(tmp39);
 
   // pad0
   Pad(&buf, 23);
@@ -2769,14 +3146,14 @@
   buf.Write(&num_classes);
 
   // this_device_mode
-  uint8_t tmp38;
-  tmp38 = static_cast<uint8_t>(this_device_mode);
-  buf.Write(&tmp38);
+  uint8_t tmp40;
+  tmp40 = static_cast<uint8_t>(this_device_mode);
+  buf.Write(&tmp40);
 
   // other_device_mode
-  uint8_t tmp39;
-  tmp39 = static_cast<uint8_t>(other_device_mode);
-  buf.Write(&tmp39);
+  uint8_t tmp41;
+  tmp41 = static_cast<uint8_t>(other_device_mode);
+  buf.Write(&tmp41);
 
   // owner_events
   buf.Write(&owner_events);
@@ -2839,9 +3216,9 @@
   Read(&length, &buf);
 
   // status
-  uint8_t tmp40;
-  Read(&tmp40, &buf);
-  status = static_cast<GrabStatus>(tmp40);
+  uint8_t tmp42;
+  Read(&tmp42, &buf);
+  status = static_cast<GrabStatus>(tmp42);
 
   // pad0
   Pad(&buf, 23);
@@ -2929,9 +3306,9 @@
   buf.Write(&num_classes);
 
   // modifiers
-  uint16_t tmp41;
-  tmp41 = static_cast<uint16_t>(modifiers);
-  buf.Write(&tmp41);
+  uint16_t tmp43;
+  tmp43 = static_cast<uint16_t>(modifiers);
+  buf.Write(&tmp43);
 
   // modifier_device
   buf.Write(&modifier_device);
@@ -2943,14 +3320,14 @@
   buf.Write(&key);
 
   // this_device_mode
-  uint8_t tmp42;
-  tmp42 = static_cast<uint8_t>(this_device_mode);
-  buf.Write(&tmp42);
+  uint8_t tmp44;
+  tmp44 = static_cast<uint8_t>(this_device_mode);
+  buf.Write(&tmp44);
 
   // other_device_mode
-  uint8_t tmp43;
-  tmp43 = static_cast<uint8_t>(other_device_mode);
-  buf.Write(&tmp43);
+  uint8_t tmp45;
+  tmp45 = static_cast<uint8_t>(other_device_mode);
+  buf.Write(&tmp45);
 
   // owner_events
   buf.Write(&owner_events);
@@ -3013,9 +3390,9 @@
   buf.Write(&grabWindow);
 
   // modifiers
-  uint16_t tmp44;
-  tmp44 = static_cast<uint16_t>(modifiers);
-  buf.Write(&tmp44);
+  uint16_t tmp46;
+  tmp46 = static_cast<uint16_t>(modifiers);
+  buf.Write(&tmp46);
 
   // modifier_device
   buf.Write(&modifier_device);
@@ -3085,19 +3462,19 @@
   buf.Write(&num_classes);
 
   // modifiers
-  uint16_t tmp45;
-  tmp45 = static_cast<uint16_t>(modifiers);
-  buf.Write(&tmp45);
+  uint16_t tmp47;
+  tmp47 = static_cast<uint16_t>(modifiers);
+  buf.Write(&tmp47);
 
   // this_device_mode
-  uint8_t tmp46;
-  tmp46 = static_cast<uint8_t>(this_device_mode);
-  buf.Write(&tmp46);
+  uint8_t tmp48;
+  tmp48 = static_cast<uint8_t>(this_device_mode);
+  buf.Write(&tmp48);
 
   // other_device_mode
-  uint8_t tmp47;
-  tmp47 = static_cast<uint8_t>(other_device_mode);
-  buf.Write(&tmp47);
+  uint8_t tmp49;
+  tmp49 = static_cast<uint8_t>(other_device_mode);
+  buf.Write(&tmp49);
 
   // button
   buf.Write(&button);
@@ -3163,9 +3540,9 @@
   buf.Write(&grab_window);
 
   // modifiers
-  uint16_t tmp48;
-  tmp48 = static_cast<uint16_t>(modifiers);
-  buf.Write(&tmp48);
+  uint16_t tmp50;
+  tmp50 = static_cast<uint16_t>(modifiers);
+  buf.Write(&tmp50);
 
   // modifier_device
   buf.Write(&modifier_device);
@@ -3221,9 +3598,9 @@
   buf.Write(&time);
 
   // mode
-  uint8_t tmp49;
-  tmp49 = static_cast<uint8_t>(mode);
-  buf.Write(&tmp49);
+  uint8_t tmp51;
+  tmp51 = static_cast<uint8_t>(mode);
+  buf.Write(&tmp51);
 
   // device_id
   buf.Write(&device_id);
@@ -3316,9 +3693,9 @@
   Read(&time, &buf);
 
   // revert_to
-  uint8_t tmp50;
-  Read(&tmp50, &buf);
-  revert_to = static_cast<InputFocus>(tmp50);
+  uint8_t tmp52;
+  Read(&tmp52, &buf);
+  revert_to = static_cast<InputFocus>(tmp52);
 
   // pad0
   Pad(&buf, 15);
@@ -3360,9 +3737,9 @@
   buf.Write(&time);
 
   // revert_to
-  uint8_t tmp51;
-  tmp51 = static_cast<uint8_t>(revert_to);
-  buf.Write(&tmp51);
+  uint8_t tmp53;
+  tmp53 = static_cast<uint8_t>(revert_to);
+  buf.Write(&tmp53);
 
   // device_id
   buf.Write(&device_id);
@@ -3465,9 +3842,9 @@
       auto& data = feedbacks_elem;
 
       // class_id
-      uint8_t tmp52;
-      Read(&tmp52, &buf);
-      class_id = static_cast<Input::FeedbackClass>(tmp52);
+      uint8_t tmp54;
+      Read(&tmp54, &buf);
+      class_id = static_cast<Input::FeedbackClass>(tmp54);
 
       // feedback_id
       Read(&feedback_id, &buf);
@@ -3635,9 +4012,9 @@
   Pad(&buf, sizeof(uint16_t));
 
   // mask
-  uint32_t tmp53;
-  tmp53 = static_cast<uint32_t>(mask);
-  buf.Write(&tmp53);
+  uint32_t tmp55;
+  tmp55 = static_cast<uint32_t>(mask);
+  buf.Write(&tmp55);
 
   // device_id
   buf.Write(&device_id);
@@ -3665,9 +4042,9 @@
               &class_id);
     SwitchVar(FeedbackClass::Led, data.led.has_value(), false, &class_id);
     SwitchVar(FeedbackClass::Bell, data.bell.has_value(), false, &class_id);
-    uint8_t tmp54;
-    tmp54 = static_cast<uint8_t>(class_id);
-    buf.Write(&tmp54);
+    uint8_t tmp56;
+    tmp56 = static_cast<uint8_t>(class_id);
+    buf.Write(&tmp56);
 
     // feedback_id
     buf.Write(&feedback_id);
@@ -4117,9 +4494,9 @@
   Read(&length, &buf);
 
   // status
-  uint8_t tmp55;
-  Read(&tmp55, &buf);
-  status = static_cast<MappingStatus>(tmp55);
+  uint8_t tmp57;
+  Read(&tmp57, &buf);
+  status = static_cast<MappingStatus>(tmp57);
 
   // pad0
   Pad(&buf, 23);
@@ -4298,9 +4675,9 @@
   Read(&length, &buf);
 
   // status
-  uint8_t tmp56;
-  Read(&tmp56, &buf);
-  status = static_cast<MappingStatus>(tmp56);
+  uint8_t tmp58;
+  Read(&tmp58, &buf);
+  status = static_cast<MappingStatus>(tmp58);
 
   // pad0
   Pad(&buf, 23);
@@ -4392,9 +4769,9 @@
       auto& data = classes_elem;
 
       // class_id
-      uint8_t tmp57;
-      Read(&tmp57, &buf);
-      class_id = static_cast<Input::InputClass>(tmp57);
+      uint8_t tmp59;
+      Read(&tmp59, &buf);
+      class_id = static_cast<Input::InputClass>(tmp59);
 
       // len
       Read(&len, &buf);
@@ -4448,9 +4825,9 @@
         Read(&num_valuators, &buf);
 
         // mode
-        uint8_t tmp58;
-        Read(&tmp58, &buf);
-        mode = static_cast<Input::ValuatorStateModeMask>(tmp58);
+        uint8_t tmp60;
+        Read(&tmp60, &buf);
+        mode = static_cast<Input::ValuatorStateModeMask>(tmp60);
 
         // valuators
         valuators.resize(num_valuators);
@@ -4601,9 +4978,9 @@
   Read(&length, &buf);
 
   // status
-  uint8_t tmp59;
-  Read(&tmp59, &buf);
-  status = static_cast<GrabStatus>(tmp59);
+  uint8_t tmp61;
+  Read(&tmp61, &buf);
+  status = static_cast<GrabStatus>(tmp61);
 
   // pad0
   Pad(&buf, 23);
@@ -4637,9 +5014,9 @@
   Pad(&buf, sizeof(uint16_t));
 
   // control_id
-  uint16_t tmp60;
-  tmp60 = static_cast<uint16_t>(control_id);
-  buf.Write(&tmp60);
+  uint16_t tmp62;
+  tmp62 = static_cast<uint16_t>(control_id);
+  buf.Write(&tmp62);
 
   // device_id
   buf.Write(&device_id);
@@ -4699,9 +5076,9 @@
     auto& data = control;
 
     // control_id
-    uint16_t tmp61;
-    Read(&tmp61, &buf);
-    control_id = static_cast<Input::DeviceControl>(tmp61);
+    uint16_t tmp63;
+    Read(&tmp63, &buf);
+    control_id = static_cast<Input::DeviceControl>(tmp63);
 
     // len
     Read(&len, &buf);
@@ -4860,9 +5237,9 @@
   Pad(&buf, sizeof(uint16_t));
 
   // control_id
-  uint16_t tmp62;
-  tmp62 = static_cast<uint16_t>(control_id);
-  buf.Write(&tmp62);
+  uint16_t tmp64;
+  tmp64 = static_cast<uint16_t>(control_id);
+  buf.Write(&tmp64);
 
   // device_id
   buf.Write(&device_id);
@@ -4886,9 +5263,9 @@
               &control_id);
     SwitchVar(DeviceControl::abs_area, data.abs_area.has_value(), false,
               &control_id);
-    uint16_t tmp63;
-    tmp63 = static_cast<uint16_t>(control_id);
-    buf.Write(&tmp63);
+    uint16_t tmp65;
+    tmp65 = static_cast<uint16_t>(control_id);
+    buf.Write(&tmp65);
 
     // len
     buf.Write(&len);
@@ -5174,14 +5551,14 @@
   SwitchVar(PropertyFormat::c_8Bits, items.data8.has_value(), false, &format);
   SwitchVar(PropertyFormat::c_16Bits, items.data16.has_value(), false, &format);
   SwitchVar(PropertyFormat::c_32Bits, items.data32.has_value(), false, &format);
-  uint8_t tmp64;
-  tmp64 = static_cast<uint8_t>(format);
-  buf.Write(&tmp64);
+  uint8_t tmp66;
+  tmp66 = static_cast<uint8_t>(format);
+  buf.Write(&tmp66);
 
   // mode
-  uint8_t tmp65;
-  tmp65 = static_cast<uint8_t>(mode);
-  buf.Write(&tmp65);
+  uint8_t tmp67;
+  tmp67 = static_cast<uint8_t>(mode);
+  buf.Write(&tmp67);
 
   // pad0
   Pad(&buf, 1);
@@ -5397,9 +5774,9 @@
   Read(&num_items, &buf);
 
   // format
-  uint8_t tmp66;
-  Read(&tmp66, &buf);
-  format = static_cast<Input::PropertyFormat>(tmp66);
+  uint8_t tmp68;
+  Read(&tmp68, &buf);
+  format = static_cast<Input::PropertyFormat>(tmp68);
 
   // device_id
   Read(&device_id, &buf);
@@ -5787,9 +6164,9 @@
                 false, &type);
       SwitchVar(HierarchyChangeType::DetachSlave, data.detach_slave.has_value(),
                 false, &type);
-      uint16_t tmp67;
-      tmp67 = static_cast<uint16_t>(type);
-      buf.Write(&tmp67);
+      uint16_t tmp69;
+      tmp69 = static_cast<uint16_t>(type);
+      buf.Write(&tmp69);
 
       // len
       buf.Write(&len);
@@ -5832,9 +6209,9 @@
         buf.Write(&deviceid);
 
         // return_mode
-        uint8_t tmp68;
-        tmp68 = static_cast<uint8_t>(return_mode);
-        buf.Write(&tmp68);
+        uint8_t tmp70;
+        tmp70 = static_cast<uint8_t>(return_mode);
+        buf.Write(&tmp70);
 
         // pad1
         Pad(&buf, 1);
@@ -6053,9 +6430,9 @@
       DCHECK_EQ(static_cast<size_t>(mask_len), mask.size());
       for (auto& mask_elem : mask) {
         // mask_elem
-        uint32_t tmp69;
-        tmp69 = static_cast<uint32_t>(mask_elem);
-        buf.Write(&tmp69);
+        uint32_t tmp71;
+        tmp71 = static_cast<uint32_t>(mask_elem);
+        buf.Write(&tmp71);
       }
     }
   }
@@ -6240,9 +6617,9 @@
       Read(&deviceid, &buf);
 
       // type
-      uint16_t tmp70;
-      Read(&tmp70, &buf);
-      type = static_cast<Input::DeviceType>(tmp70);
+      uint16_t tmp72;
+      Read(&tmp72, &buf);
+      type = static_cast<Input::DeviceType>(tmp72);
 
       // attachment
       Read(&attachment, &buf);
@@ -6280,9 +6657,9 @@
           auto& data = classes_elem;
 
           // type
-          uint16_t tmp71;
-          Read(&tmp71, &buf);
-          type = static_cast<Input::DeviceClassType>(tmp71);
+          uint16_t tmp73;
+          Read(&tmp73, &buf);
+          type = static_cast<Input::DeviceClassType>(tmp73);
 
           // len
           Read(&len, &buf);
@@ -6389,9 +6766,9 @@
             Read(&resolution, &buf);
 
             // mode
-            uint8_t tmp72;
-            Read(&tmp72, &buf);
-            mode = static_cast<Input::ValuatorMode>(tmp72);
+            uint8_t tmp74;
+            Read(&tmp74, &buf);
+            mode = static_cast<Input::ValuatorMode>(tmp74);
 
             // pad0
             Pad(&buf, 3);
@@ -6407,17 +6784,17 @@
             Read(&number, &buf);
 
             // scroll_type
-            uint16_t tmp73;
-            Read(&tmp73, &buf);
-            scroll_type = static_cast<Input::ScrollType>(tmp73);
+            uint16_t tmp75;
+            Read(&tmp75, &buf);
+            scroll_type = static_cast<Input::ScrollType>(tmp75);
 
             // pad1
             Pad(&buf, 2);
 
             // flags
-            uint32_t tmp74;
-            Read(&tmp74, &buf);
-            flags = static_cast<Input::ScrollFlags>(tmp74);
+            uint32_t tmp76;
+            Read(&tmp76, &buf);
+            flags = static_cast<Input::ScrollFlags>(tmp76);
 
             // increment
             {
@@ -6437,13 +6814,23 @@
             auto& num_touches = (*data.touch).num_touches;
 
             // mode
-            uint8_t tmp75;
-            Read(&tmp75, &buf);
-            mode = static_cast<Input::TouchMode>(tmp75);
+            uint8_t tmp77;
+            Read(&tmp77, &buf);
+            mode = static_cast<Input::TouchMode>(tmp77);
 
             // num_touches
             Read(&num_touches, &buf);
           }
+          if (CaseEq(data_expr, Input::DeviceClassType::Gesture)) {
+            data.gesture.emplace();
+            auto& num_touches = (*data.gesture).num_touches;
+
+            // num_touches
+            Read(&num_touches, &buf);
+
+            // pad2
+            Pad(&buf, 1);
+          }
         }
       }
     }
@@ -6615,19 +7002,19 @@
   buf.Write(&deviceid);
 
   // mode
-  uint8_t tmp76;
-  tmp76 = static_cast<uint8_t>(mode);
-  buf.Write(&tmp76);
+  uint8_t tmp78;
+  tmp78 = static_cast<uint8_t>(mode);
+  buf.Write(&tmp78);
 
   // paired_device_mode
-  uint8_t tmp77;
-  tmp77 = static_cast<uint8_t>(paired_device_mode);
-  buf.Write(&tmp77);
+  uint8_t tmp79;
+  tmp79 = static_cast<uint8_t>(paired_device_mode);
+  buf.Write(&tmp79);
 
   // owner_events
-  uint8_t tmp78;
-  tmp78 = static_cast<uint8_t>(owner_events);
-  buf.Write(&tmp78);
+  uint8_t tmp80;
+  tmp80 = static_cast<uint8_t>(owner_events);
+  buf.Write(&tmp80);
 
   // pad0
   Pad(&buf, 1);
@@ -6688,9 +7075,9 @@
   Read(&length, &buf);
 
   // status
-  uint8_t tmp79;
-  Read(&tmp79, &buf);
-  status = static_cast<GrabStatus>(tmp79);
+  uint8_t tmp81;
+  Read(&tmp81, &buf);
+  status = static_cast<GrabStatus>(tmp81);
 
   // pad1
   Pad(&buf, 23);
@@ -6772,9 +7159,9 @@
   buf.Write(&deviceid);
 
   // event_mode
-  uint8_t tmp80;
-  tmp80 = static_cast<uint8_t>(event_mode);
-  buf.Write(&tmp80);
+  uint8_t tmp82;
+  tmp82 = static_cast<uint8_t>(event_mode);
+  buf.Write(&tmp82);
 
   // pad0
   Pad(&buf, 1);
@@ -6857,25 +7244,25 @@
   buf.Write(&mask_len);
 
   // grab_type
-  uint8_t tmp81;
-  tmp81 = static_cast<uint8_t>(grab_type);
-  buf.Write(&tmp81);
-
-  // grab_mode
-  uint8_t tmp82;
-  tmp82 = static_cast<uint8_t>(grab_mode);
-  buf.Write(&tmp82);
-
-  // paired_device_mode
   uint8_t tmp83;
-  tmp83 = static_cast<uint8_t>(paired_device_mode);
+  tmp83 = static_cast<uint8_t>(grab_type);
   buf.Write(&tmp83);
 
-  // owner_events
+  // grab_mode
   uint8_t tmp84;
-  tmp84 = static_cast<uint8_t>(owner_events);
+  tmp84 = static_cast<uint8_t>(grab_mode);
   buf.Write(&tmp84);
 
+  // paired_device_mode
+  uint8_t tmp85;
+  tmp85 = static_cast<uint8_t>(paired_device_mode);
+  buf.Write(&tmp85);
+
+  // owner_events
+  uint8_t tmp86;
+  tmp86 = static_cast<uint8_t>(owner_events);
+  buf.Write(&tmp86);
+
   // pad0
   Pad(&buf, 2);
 
@@ -6960,9 +7347,9 @@
       Read(&modifiers, &buf);
 
       // status
-      uint8_t tmp85;
-      Read(&tmp85, &buf);
-      status = static_cast<GrabStatus>(tmp85);
+      uint8_t tmp87;
+      Read(&tmp87, &buf);
+      status = static_cast<GrabStatus>(tmp87);
 
       // pad0
       Pad(&buf, 3);
@@ -7016,9 +7403,9 @@
   buf.Write(&num_modifiers);
 
   // grab_type
-  uint8_t tmp86;
-  tmp86 = static_cast<uint8_t>(grab_type);
-  buf.Write(&tmp86);
+  uint8_t tmp88;
+  tmp88 = static_cast<uint8_t>(grab_type);
+  buf.Write(&tmp88);
 
   // pad0
   Pad(&buf, 3);
@@ -7160,17 +7547,17 @@
   buf.Write(&deviceid);
 
   // mode
-  uint8_t tmp87;
-  tmp87 = static_cast<uint8_t>(mode);
-  buf.Write(&tmp87);
+  uint8_t tmp89;
+  tmp89 = static_cast<uint8_t>(mode);
+  buf.Write(&tmp89);
 
   // format
   SwitchVar(PropertyFormat::c_8Bits, items.data8.has_value(), false, &format);
   SwitchVar(PropertyFormat::c_16Bits, items.data16.has_value(), false, &format);
   SwitchVar(PropertyFormat::c_32Bits, items.data32.has_value(), false, &format);
-  uint8_t tmp88;
-  tmp88 = static_cast<uint8_t>(format);
-  buf.Write(&tmp88);
+  uint8_t tmp90;
+  tmp90 = static_cast<uint8_t>(format);
+  buf.Write(&tmp90);
 
   // property
   buf.Write(&property);
@@ -7384,9 +7771,9 @@
   Read(&num_items, &buf);
 
   // format
-  uint8_t tmp89;
-  Read(&tmp89, &buf);
-  format = static_cast<Input::PropertyFormat>(tmp89);
+  uint8_t tmp91;
+  Read(&tmp91, &buf);
+  format = static_cast<Input::PropertyFormat>(tmp91);
 
   // pad1
   Pad(&buf, 11);
@@ -7528,9 +7915,9 @@
       mask.resize(mask_len);
       for (auto& mask_elem : mask) {
         // mask_elem
-        uint32_t tmp90;
-        Read(&tmp90, &buf);
-        mask_elem = static_cast<Input::XIEventMask>(tmp90);
+        uint32_t tmp92;
+        Read(&tmp92, &buf);
+        mask_elem = static_cast<Input::XIEventMask>(tmp92);
       }
     }
   }
diff --git a/ui/gfx/x/generated_protos/xinput.h b/ui/gfx/x/generated_protos/xinput.h
index 1c9b676d..31651f2 100644
--- a/ui/gfx/x/generated_protos/xinput.h
+++ b/ui/gfx/x/generated_protos/xinput.h
@@ -70,7 +70,7 @@
 class COMPONENT_EXPORT(X11) Input {
  public:
   static constexpr unsigned major_version = 2;
-  static constexpr unsigned minor_version = 3;
+  static constexpr unsigned minor_version = 4;
 
   Input(Connection* connection, const x11::QueryExtensionReply& info);
 
@@ -226,6 +226,7 @@
     Valuator = 2,
     Scroll = 3,
     Touch = 8,
+    Gesture = 9,
   };
 
   enum class DeviceType : int {
@@ -279,6 +280,8 @@
     Enter = 2,
     FocusIn = 3,
     TouchBegin = 4,
+    GesturePinchBegin = 5,
+    GestureSwipeBegin = 6,
   };
 
   enum class ModifierMask : int {
@@ -375,6 +378,14 @@
     DeviceIsGrabbed = 1 << 1,
   };
 
+  enum class GesturePinchEventFlags : int {
+    GesturePinchCancelled = 1 << 0,
+  };
+
+  enum class GestureSwipeEventFlags : int {
+    GestureSwipeCancelled = 1 << 0,
+  };
+
   struct Fp3232 {
     bool operator==(const Fp3232& other) const {
       return integral == other.integral && frac == other.frac;
@@ -1222,6 +1233,18 @@
     uint8_t num_touches{};
   };
 
+  struct GestureClass {
+    bool operator==(const GestureClass& other) const {
+      return type == other.type && len == other.len &&
+             sourceid == other.sourceid && num_touches == other.num_touches;
+    }
+
+    DeviceClassType type{};
+    uint16_t len{};
+    DeviceId sourceid{};
+    uint8_t num_touches{};
+  };
+
   struct ValuatorClass {
     bool operator==(const ValuatorClass& other) const {
       return type == other.type && len == other.len &&
@@ -1272,11 +1295,15 @@
       TouchMode mode{};
       uint8_t num_touches{};
     };
+    struct Gesture {
+      uint8_t num_touches{};
+    };
     absl::optional<Key> key{};
     absl::optional<Button> button{};
     absl::optional<Valuator> valuator{};
     absl::optional<Scroll> scroll{};
     absl::optional<Touch> touch{};
+    absl::optional<Gesture> gesture{};
   };
 
   struct XIDeviceInfo {
@@ -1630,33 +1657,110 @@
     x11::Window* GetWindow() { return reinterpret_cast<x11::Window*>(&event); }
   };
 
+  struct GesturePinchEvent {
+    static constexpr int type_id = 38;
+    enum Opcode {
+      Begin = 27,
+      Update = 28,
+      End = 29,
+    } opcode{};
+    uint16_t sequence{};
+    DeviceId deviceid{};
+    Time time{};
+    uint32_t detail{};
+    Window root{};
+    Window event{};
+    Window child{};
+    Fp1616 root_x{};
+    Fp1616 root_y{};
+    Fp1616 event_x{};
+    Fp1616 event_y{};
+    Fp1616 delta_x{};
+    Fp1616 delta_y{};
+    Fp1616 delta_unaccel_x{};
+    Fp1616 delta_unaccel_y{};
+    Fp1616 scale{};
+    Fp1616 delta_angle{};
+    DeviceId sourceid{};
+    ModifierInfo mods{};
+    GroupInfo group{};
+    GesturePinchEventFlags flags{};
+
+    x11::Window* GetWindow() { return reinterpret_cast<x11::Window*>(&event); }
+  };
+
+  struct GestureSwipeEvent {
+    static constexpr int type_id = 39;
+    enum Opcode {
+      Begin = 30,
+      Update = 31,
+      End = 32,
+    } opcode{};
+    uint16_t sequence{};
+    DeviceId deviceid{};
+    Time time{};
+    uint32_t detail{};
+    Window root{};
+    Window event{};
+    Window child{};
+    Fp1616 root_x{};
+    Fp1616 root_y{};
+    Fp1616 event_x{};
+    Fp1616 event_y{};
+    Fp1616 delta_x{};
+    Fp1616 delta_y{};
+    Fp1616 delta_unaccel_x{};
+    Fp1616 delta_unaccel_y{};
+    DeviceId sourceid{};
+    ModifierInfo mods{};
+    GroupInfo group{};
+    GestureSwipeEventFlags flags{};
+
+    x11::Window* GetWindow() { return reinterpret_cast<x11::Window*>(&event); }
+  };
+
   using EventForSend = std::array<uint8_t, 32>;
   struct DeviceError : public x11::Error {
     uint16_t sequence{};
+    uint32_t bad_value{};
+    uint16_t minor_opcode{};
+    uint8_t major_opcode{};
 
     std::string ToString() const override;
   };
 
   struct EventError : public x11::Error {
     uint16_t sequence{};
+    uint32_t bad_value{};
+    uint16_t minor_opcode{};
+    uint8_t major_opcode{};
 
     std::string ToString() const override;
   };
 
   struct ModeError : public x11::Error {
     uint16_t sequence{};
+    uint32_t bad_value{};
+    uint16_t minor_opcode{};
+    uint8_t major_opcode{};
 
     std::string ToString() const override;
   };
 
   struct DeviceBusyError : public x11::Error {
     uint16_t sequence{};
+    uint32_t bad_value{};
+    uint16_t minor_opcode{};
+    uint8_t major_opcode{};
 
     std::string ToString() const override;
   };
 
   struct ClassError : public x11::Error {
     uint16_t sequence{};
+    uint32_t bad_value{};
+    uint16_t minor_opcode{};
+    uint8_t major_opcode{};
 
     std::string ToString() const override;
   };
@@ -3501,4 +3605,36 @@
                                                static_cast<T>(r));
 }
 
+inline constexpr x11::Input::GesturePinchEventFlags operator|(
+    x11::Input::GesturePinchEventFlags l,
+    x11::Input::GesturePinchEventFlags r) {
+  using T = std::underlying_type_t<x11::Input::GesturePinchEventFlags>;
+  return static_cast<x11::Input::GesturePinchEventFlags>(static_cast<T>(l) |
+                                                         static_cast<T>(r));
+}
+
+inline constexpr x11::Input::GesturePinchEventFlags operator&(
+    x11::Input::GesturePinchEventFlags l,
+    x11::Input::GesturePinchEventFlags r) {
+  using T = std::underlying_type_t<x11::Input::GesturePinchEventFlags>;
+  return static_cast<x11::Input::GesturePinchEventFlags>(static_cast<T>(l) &
+                                                         static_cast<T>(r));
+}
+
+inline constexpr x11::Input::GestureSwipeEventFlags operator|(
+    x11::Input::GestureSwipeEventFlags l,
+    x11::Input::GestureSwipeEventFlags r) {
+  using T = std::underlying_type_t<x11::Input::GestureSwipeEventFlags>;
+  return static_cast<x11::Input::GestureSwipeEventFlags>(static_cast<T>(l) |
+                                                         static_cast<T>(r));
+}
+
+inline constexpr x11::Input::GestureSwipeEventFlags operator&(
+    x11::Input::GestureSwipeEventFlags l,
+    x11::Input::GestureSwipeEventFlags r) {
+  using T = std::underlying_type_t<x11::Input::GestureSwipeEventFlags>;
+  return static_cast<x11::Input::GestureSwipeEventFlags>(static_cast<T>(l) &
+                                                         static_cast<T>(r));
+}
+
 #endif  // UI_GFX_X_GENERATED_PROTOS_XINPUT_H_
diff --git a/ui/gfx/x/generated_protos/xkb.h b/ui/gfx/x/generated_protos/xkb.h
index 79e326ca..6270f287 100644
--- a/ui/gfx/x/generated_protos/xkb.h
+++ b/ui/gfx/x/generated_protos/xkb.h
@@ -1194,7 +1194,7 @@
   static_assert(std::is_trivially_copyable<Action>::value, "");
 
   struct NewKeyboardNotifyEvent {
-    static constexpr int type_id = 38;
+    static constexpr int type_id = 40;
     static constexpr uint8_t opcode = 0;
     uint8_t xkbType{};
     uint16_t sequence{};
@@ -1213,7 +1213,7 @@
   };
 
   struct MapNotifyEvent {
-    static constexpr int type_id = 39;
+    static constexpr int type_id = 41;
     static constexpr uint8_t opcode = 1;
     uint8_t xkbType{};
     uint16_t sequence{};
@@ -1243,7 +1243,7 @@
   };
 
   struct StateNotifyEvent {
-    static constexpr int type_id = 40;
+    static constexpr int type_id = 42;
     static constexpr uint8_t opcode = 2;
     uint8_t xkbType{};
     uint16_t sequence{};
@@ -1273,7 +1273,7 @@
   };
 
   struct ControlsNotifyEvent {
-    static constexpr int type_id = 41;
+    static constexpr int type_id = 43;
     static constexpr uint8_t opcode = 3;
     uint8_t xkbType{};
     uint16_t sequence{};
@@ -1292,7 +1292,7 @@
   };
 
   struct IndicatorStateNotifyEvent {
-    static constexpr int type_id = 42;
+    static constexpr int type_id = 44;
     static constexpr uint8_t opcode = 4;
     uint8_t xkbType{};
     uint16_t sequence{};
@@ -1305,7 +1305,7 @@
   };
 
   struct IndicatorMapNotifyEvent {
-    static constexpr int type_id = 43;
+    static constexpr int type_id = 45;
     static constexpr uint8_t opcode = 5;
     uint8_t xkbType{};
     uint16_t sequence{};
@@ -1318,7 +1318,7 @@
   };
 
   struct NamesNotifyEvent {
-    static constexpr int type_id = 44;
+    static constexpr int type_id = 46;
     static constexpr uint8_t opcode = 6;
     uint8_t xkbType{};
     uint16_t sequence{};
@@ -1341,7 +1341,7 @@
   };
 
   struct CompatMapNotifyEvent {
-    static constexpr int type_id = 45;
+    static constexpr int type_id = 47;
     static constexpr uint8_t opcode = 7;
     uint8_t xkbType{};
     uint16_t sequence{};
@@ -1356,7 +1356,7 @@
   };
 
   struct BellNotifyEvent {
-    static constexpr int type_id = 46;
+    static constexpr int type_id = 48;
     static constexpr uint8_t opcode = 8;
     uint8_t xkbType{};
     uint16_t sequence{};
@@ -1375,7 +1375,7 @@
   };
 
   struct ActionMessageEvent {
-    static constexpr int type_id = 47;
+    static constexpr int type_id = 49;
     static constexpr uint8_t opcode = 9;
     uint8_t xkbType{};
     uint16_t sequence{};
@@ -1392,7 +1392,7 @@
   };
 
   struct AccessXNotifyEvent {
-    static constexpr int type_id = 48;
+    static constexpr int type_id = 50;
     static constexpr uint8_t opcode = 10;
     uint8_t xkbType{};
     uint16_t sequence{};
@@ -1407,7 +1407,7 @@
   };
 
   struct ExtensionDeviceNotifyEvent {
-    static constexpr int type_id = 49;
+    static constexpr int type_id = 51;
     static constexpr uint8_t opcode = 11;
     uint8_t xkbType{};
     uint16_t sequence{};
diff --git a/ui/gfx/x/generated_protos/xprint.cc b/ui/gfx/x/generated_protos/xprint.cc
index 01f586a..e2c236af 100644
--- a/ui/gfx/x/generated_protos/xprint.cc
+++ b/ui/gfx/x/generated_protos/xprint.cc
@@ -112,7 +112,10 @@
 std::string XPrint::BadContextError::ToString() const {
   std::stringstream ss_;
   ss_ << "XPrint::BadContextError{";
-  ss_ << ".sequence = " << static_cast<uint64_t>(sequence);
+  ss_ << ".sequence = " << static_cast<uint64_t>(sequence) << ", ";
+  ss_ << ".bad_value = " << static_cast<uint64_t>(bad_value) << ", ";
+  ss_ << ".minor_opcode = " << static_cast<uint64_t>(minor_opcode) << ", ";
+  ss_ << ".major_opcode = " << static_cast<uint64_t>(major_opcode);
   ss_ << "}";
   return ss_.str();
 }
@@ -123,6 +126,9 @@
   auto& buf = *buffer;
 
   auto& sequence = (*error_).sequence;
+  auto& bad_value = (*error_).bad_value;
+  auto& minor_opcode = (*error_).minor_opcode;
+  auto& major_opcode = (*error_).major_opcode;
 
   // response_type
   uint8_t response_type;
@@ -135,12 +141,24 @@
   // sequence
   Read(&sequence, &buf);
 
+  // bad_value
+  Read(&bad_value, &buf);
+
+  // minor_opcode
+  Read(&minor_opcode, &buf);
+
+  // major_opcode
+  Read(&major_opcode, &buf);
+
   DCHECK_LE(buf.offset, 32ul);
 }
 std::string XPrint::BadSequenceError::ToString() const {
   std::stringstream ss_;
   ss_ << "XPrint::BadSequenceError{";
-  ss_ << ".sequence = " << static_cast<uint64_t>(sequence);
+  ss_ << ".sequence = " << static_cast<uint64_t>(sequence) << ", ";
+  ss_ << ".bad_value = " << static_cast<uint64_t>(bad_value) << ", ";
+  ss_ << ".minor_opcode = " << static_cast<uint64_t>(minor_opcode) << ", ";
+  ss_ << ".major_opcode = " << static_cast<uint64_t>(major_opcode);
   ss_ << "}";
   return ss_.str();
 }
@@ -151,6 +169,9 @@
   auto& buf = *buffer;
 
   auto& sequence = (*error_).sequence;
+  auto& bad_value = (*error_).bad_value;
+  auto& minor_opcode = (*error_).minor_opcode;
+  auto& major_opcode = (*error_).major_opcode;
 
   // response_type
   uint8_t response_type;
@@ -163,6 +184,15 @@
   // sequence
   Read(&sequence, &buf);
 
+  // bad_value
+  Read(&bad_value, &buf);
+
+  // minor_opcode
+  Read(&minor_opcode, &buf);
+
+  // major_opcode
+  Read(&major_opcode, &buf);
+
   DCHECK_LE(buf.offset, 32ul);
 }
 Future<XPrint::PrintQueryVersionReply> XPrint::PrintQueryVersion(
@@ -272,6 +302,9 @@
     buf.Write(&printer_name_elem);
   }
 
+  // pad0
+  Align(&buf, 4);
+
   // locale
   DCHECK_EQ(static_cast<size_t>(localeLen), locale.size());
   for (auto& locale_elem : locale) {
@@ -445,6 +478,9 @@
     buf.Write(&printerName_elem);
   }
 
+  // pad0
+  Align(&buf, 4);
+
   // locale
   DCHECK_EQ(static_cast<size_t>(localeLen), locale.size());
   for (auto& locale_elem : locale) {
@@ -839,6 +875,9 @@
     buf.Write(&data_elem);
   }
 
+  // pad0
+  Align(&buf, 4);
+
   // doc_format
   DCHECK_EQ(static_cast<size_t>(len_fmt), doc_format.size());
   for (auto& doc_format_elem : doc_format) {
@@ -846,6 +885,9 @@
     buf.Write(&doc_format_elem);
   }
 
+  // pad1
+  Align(&buf, 4);
+
   // options
   DCHECK_EQ(static_cast<size_t>(len_options), options.size());
   for (auto& options_elem : options) {
diff --git a/ui/gfx/x/generated_protos/xprint.h b/ui/gfx/x/generated_protos/xprint.h
index 76f4138..ec0170f 100644
--- a/ui/gfx/x/generated_protos/xprint.h
+++ b/ui/gfx/x/generated_protos/xprint.h
@@ -124,7 +124,7 @@
   };
 
   struct NotifyEvent {
-    static constexpr int type_id = 50;
+    static constexpr int type_id = 52;
     static constexpr uint8_t opcode = 0;
     uint8_t detail{};
     uint16_t sequence{};
@@ -135,7 +135,7 @@
   };
 
   struct AttributNotifyEvent {
-    static constexpr int type_id = 51;
+    static constexpr int type_id = 53;
     static constexpr uint8_t opcode = 1;
     uint8_t detail{};
     uint16_t sequence{};
@@ -146,12 +146,18 @@
 
   struct BadContextError : public x11::Error {
     uint16_t sequence{};
+    uint32_t bad_value{};
+    uint16_t minor_opcode{};
+    uint8_t major_opcode{};
 
     std::string ToString() const override;
   };
 
   struct BadSequenceError : public x11::Error {
     uint16_t sequence{};
+    uint32_t bad_value{};
+    uint16_t minor_opcode{};
+    uint8_t major_opcode{};
 
     std::string ToString() const override;
   };
diff --git a/ui/gfx/x/generated_protos/xproto.h b/ui/gfx/x/generated_protos/xproto.h
index f6349a3..5d6daa7 100644
--- a/ui/gfx/x/generated_protos/xproto.h
+++ b/ui/gfx/x/generated_protos/xproto.h
@@ -895,7 +895,7 @@
 };
 
 struct KeyEvent {
-  static constexpr int type_id = 52;
+  static constexpr int type_id = 54;
   enum Opcode {
     Press = 2,
     Release = 3,
@@ -917,7 +917,7 @@
 };
 
 struct ButtonEvent {
-  static constexpr int type_id = 53;
+  static constexpr int type_id = 55;
   enum Opcode {
     Press = 4,
     Release = 5,
@@ -939,7 +939,7 @@
 };
 
 struct MotionNotifyEvent {
-  static constexpr int type_id = 54;
+  static constexpr int type_id = 56;
   static constexpr uint8_t opcode = 6;
   Motion detail{};
   uint16_t sequence{};
@@ -958,7 +958,7 @@
 };
 
 struct CrossingEvent {
-  static constexpr int type_id = 55;
+  static constexpr int type_id = 57;
   enum Opcode {
     EnterNotify = 7,
     LeaveNotify = 8,
@@ -981,7 +981,7 @@
 };
 
 struct FocusEvent {
-  static constexpr int type_id = 56;
+  static constexpr int type_id = 58;
   enum Opcode {
     In = 9,
     Out = 10,
@@ -995,7 +995,7 @@
 };
 
 struct KeymapNotifyEvent {
-  static constexpr int type_id = 57;
+  static constexpr int type_id = 59;
   static constexpr uint8_t opcode = 11;
   std::array<uint8_t, 31> keys{};
 
@@ -1003,7 +1003,7 @@
 };
 
 struct ExposeEvent {
-  static constexpr int type_id = 58;
+  static constexpr int type_id = 60;
   static constexpr uint8_t opcode = 12;
   uint16_t sequence{};
   Window window{};
@@ -1017,7 +1017,7 @@
 };
 
 struct GraphicsExposureEvent {
-  static constexpr int type_id = 59;
+  static constexpr int type_id = 61;
   static constexpr uint8_t opcode = 13;
   uint16_t sequence{};
   Drawable drawable{};
@@ -1033,7 +1033,7 @@
 };
 
 struct NoExposureEvent {
-  static constexpr int type_id = 60;
+  static constexpr int type_id = 62;
   static constexpr uint8_t opcode = 14;
   uint16_t sequence{};
   Drawable drawable{};
@@ -1044,7 +1044,7 @@
 };
 
 struct VisibilityNotifyEvent {
-  static constexpr int type_id = 61;
+  static constexpr int type_id = 63;
   static constexpr uint8_t opcode = 15;
   uint16_t sequence{};
   Window window{};
@@ -1054,7 +1054,7 @@
 };
 
 struct CreateNotifyEvent {
-  static constexpr int type_id = 62;
+  static constexpr int type_id = 64;
   static constexpr uint8_t opcode = 16;
   uint16_t sequence{};
   Window parent{};
@@ -1070,7 +1070,7 @@
 };
 
 struct DestroyNotifyEvent {
-  static constexpr int type_id = 63;
+  static constexpr int type_id = 65;
   static constexpr uint8_t opcode = 17;
   uint16_t sequence{};
   Window event{};
@@ -1080,7 +1080,7 @@
 };
 
 struct UnmapNotifyEvent {
-  static constexpr int type_id = 64;
+  static constexpr int type_id = 66;
   static constexpr uint8_t opcode = 18;
   uint16_t sequence{};
   Window event{};
@@ -1091,7 +1091,7 @@
 };
 
 struct MapNotifyEvent {
-  static constexpr int type_id = 65;
+  static constexpr int type_id = 67;
   static constexpr uint8_t opcode = 19;
   uint16_t sequence{};
   Window event{};
@@ -1102,7 +1102,7 @@
 };
 
 struct MapRequestEvent {
-  static constexpr int type_id = 66;
+  static constexpr int type_id = 68;
   static constexpr uint8_t opcode = 20;
   uint16_t sequence{};
   Window parent{};
@@ -1112,7 +1112,7 @@
 };
 
 struct ReparentNotifyEvent {
-  static constexpr int type_id = 67;
+  static constexpr int type_id = 69;
   static constexpr uint8_t opcode = 21;
   uint16_t sequence{};
   Window event{};
@@ -1126,7 +1126,7 @@
 };
 
 struct ConfigureNotifyEvent {
-  static constexpr int type_id = 68;
+  static constexpr int type_id = 70;
   static constexpr uint8_t opcode = 22;
   uint16_t sequence{};
   Window event{};
@@ -1143,7 +1143,7 @@
 };
 
 struct ConfigureRequestEvent {
-  static constexpr int type_id = 69;
+  static constexpr int type_id = 71;
   static constexpr uint8_t opcode = 23;
   StackMode stack_mode{};
   uint16_t sequence{};
@@ -1161,7 +1161,7 @@
 };
 
 struct GravityNotifyEvent {
-  static constexpr int type_id = 70;
+  static constexpr int type_id = 72;
   static constexpr uint8_t opcode = 24;
   uint16_t sequence{};
   Window event{};
@@ -1173,7 +1173,7 @@
 };
 
 struct ResizeRequestEvent {
-  static constexpr int type_id = 71;
+  static constexpr int type_id = 73;
   static constexpr uint8_t opcode = 25;
   uint16_t sequence{};
   Window window{};
@@ -1184,7 +1184,7 @@
 };
 
 struct CirculateEvent {
-  static constexpr int type_id = 72;
+  static constexpr int type_id = 74;
   enum Opcode {
     Notify = 26,
     Request = 27,
@@ -1198,7 +1198,7 @@
 };
 
 struct PropertyNotifyEvent {
-  static constexpr int type_id = 73;
+  static constexpr int type_id = 75;
   static constexpr uint8_t opcode = 28;
   uint16_t sequence{};
   Window window{};
@@ -1210,7 +1210,7 @@
 };
 
 struct SelectionClearEvent {
-  static constexpr int type_id = 74;
+  static constexpr int type_id = 76;
   static constexpr uint8_t opcode = 29;
   uint16_t sequence{};
   Time time{};
@@ -1221,7 +1221,7 @@
 };
 
 struct SelectionRequestEvent {
-  static constexpr int type_id = 75;
+  static constexpr int type_id = 77;
   static constexpr uint8_t opcode = 30;
   uint16_t sequence{};
   Time time{};
@@ -1235,7 +1235,7 @@
 };
 
 struct SelectionNotifyEvent {
-  static constexpr int type_id = 76;
+  static constexpr int type_id = 78;
   static constexpr uint8_t opcode = 31;
   uint16_t sequence{};
   Time time{};
@@ -1250,7 +1250,7 @@
 };
 
 struct ColormapNotifyEvent {
-  static constexpr int type_id = 77;
+  static constexpr int type_id = 79;
   static constexpr uint8_t opcode = 32;
   uint16_t sequence{};
   Window window{};
@@ -1271,7 +1271,7 @@
 static_assert(std::is_trivially_copyable<ClientMessageData>::value, "");
 
 struct ClientMessageEvent {
-  static constexpr int type_id = 78;
+  static constexpr int type_id = 80;
   static constexpr uint8_t opcode = 33;
   uint8_t format{};
   uint16_t sequence{};
@@ -1283,7 +1283,7 @@
 };
 
 struct MappingNotifyEvent {
-  static constexpr int type_id = 79;
+  static constexpr int type_id = 81;
   static constexpr uint8_t opcode = 34;
   uint16_t sequence{};
   Mapping request{};
@@ -1294,7 +1294,7 @@
 };
 
 struct GeGenericEvent {
-  static constexpr int type_id = 80;
+  static constexpr int type_id = 82;
   static constexpr uint8_t opcode = 35;
   uint16_t sequence{};
 
diff --git a/ui/gfx/x/generated_protos/xv.cc b/ui/gfx/x/generated_protos/xv.cc
index c206f2d..9150830 100644
--- a/ui/gfx/x/generated_protos/xv.cc
+++ b/ui/gfx/x/generated_protos/xv.cc
@@ -56,7 +56,10 @@
 std::string Xv::BadPortError::ToString() const {
   std::stringstream ss_;
   ss_ << "Xv::BadPortError{";
-  ss_ << ".sequence = " << static_cast<uint64_t>(sequence);
+  ss_ << ".sequence = " << static_cast<uint64_t>(sequence) << ", ";
+  ss_ << ".bad_value = " << static_cast<uint64_t>(bad_value) << ", ";
+  ss_ << ".minor_opcode = " << static_cast<uint64_t>(minor_opcode) << ", ";
+  ss_ << ".major_opcode = " << static_cast<uint64_t>(major_opcode);
   ss_ << "}";
   return ss_.str();
 }
@@ -66,6 +69,9 @@
   auto& buf = *buffer;
 
   auto& sequence = (*error_).sequence;
+  auto& bad_value = (*error_).bad_value;
+  auto& minor_opcode = (*error_).minor_opcode;
+  auto& major_opcode = (*error_).major_opcode;
 
   // response_type
   uint8_t response_type;
@@ -78,12 +84,24 @@
   // sequence
   Read(&sequence, &buf);
 
+  // bad_value
+  Read(&bad_value, &buf);
+
+  // minor_opcode
+  Read(&minor_opcode, &buf);
+
+  // major_opcode
+  Read(&major_opcode, &buf);
+
   DCHECK_LE(buf.offset, 32ul);
 }
 std::string Xv::BadEncodingError::ToString() const {
   std::stringstream ss_;
   ss_ << "Xv::BadEncodingError{";
-  ss_ << ".sequence = " << static_cast<uint64_t>(sequence);
+  ss_ << ".sequence = " << static_cast<uint64_t>(sequence) << ", ";
+  ss_ << ".bad_value = " << static_cast<uint64_t>(bad_value) << ", ";
+  ss_ << ".minor_opcode = " << static_cast<uint64_t>(minor_opcode) << ", ";
+  ss_ << ".major_opcode = " << static_cast<uint64_t>(major_opcode);
   ss_ << "}";
   return ss_.str();
 }
@@ -94,6 +112,9 @@
   auto& buf = *buffer;
 
   auto& sequence = (*error_).sequence;
+  auto& bad_value = (*error_).bad_value;
+  auto& minor_opcode = (*error_).minor_opcode;
+  auto& major_opcode = (*error_).major_opcode;
 
   // response_type
   uint8_t response_type;
@@ -106,12 +127,24 @@
   // sequence
   Read(&sequence, &buf);
 
+  // bad_value
+  Read(&bad_value, &buf);
+
+  // minor_opcode
+  Read(&minor_opcode, &buf);
+
+  // major_opcode
+  Read(&major_opcode, &buf);
+
   DCHECK_LE(buf.offset, 32ul);
 }
 std::string Xv::BadControlError::ToString() const {
   std::stringstream ss_;
   ss_ << "Xv::BadControlError{";
-  ss_ << ".sequence = " << static_cast<uint64_t>(sequence);
+  ss_ << ".sequence = " << static_cast<uint64_t>(sequence) << ", ";
+  ss_ << ".bad_value = " << static_cast<uint64_t>(bad_value) << ", ";
+  ss_ << ".minor_opcode = " << static_cast<uint64_t>(minor_opcode) << ", ";
+  ss_ << ".major_opcode = " << static_cast<uint64_t>(major_opcode);
   ss_ << "}";
   return ss_.str();
 }
@@ -122,6 +155,9 @@
   auto& buf = *buffer;
 
   auto& sequence = (*error_).sequence;
+  auto& bad_value = (*error_).bad_value;
+  auto& minor_opcode = (*error_).minor_opcode;
+  auto& major_opcode = (*error_).major_opcode;
 
   // response_type
   uint8_t response_type;
@@ -134,6 +170,15 @@
   // sequence
   Read(&sequence, &buf);
 
+  // bad_value
+  Read(&bad_value, &buf);
+
+  // minor_opcode
+  Read(&minor_opcode, &buf);
+
+  // major_opcode
+  Read(&major_opcode, &buf);
+
   DCHECK_LE(buf.offset, 32ul);
 }
 template <>
diff --git a/ui/gfx/x/generated_protos/xv.h b/ui/gfx/x/generated_protos/xv.h
index 0c5f76bf..0800d15f 100644
--- a/ui/gfx/x/generated_protos/xv.h
+++ b/ui/gfx/x/generated_protos/xv.h
@@ -248,24 +248,33 @@
 
   struct BadPortError : public x11::Error {
     uint16_t sequence{};
+    uint32_t bad_value{};
+    uint16_t minor_opcode{};
+    uint8_t major_opcode{};
 
     std::string ToString() const override;
   };
 
   struct BadEncodingError : public x11::Error {
     uint16_t sequence{};
+    uint32_t bad_value{};
+    uint16_t minor_opcode{};
+    uint8_t major_opcode{};
 
     std::string ToString() const override;
   };
 
   struct BadControlError : public x11::Error {
     uint16_t sequence{};
+    uint32_t bad_value{};
+    uint16_t minor_opcode{};
+    uint8_t major_opcode{};
 
     std::string ToString() const override;
   };
 
   struct VideoNotifyEvent {
-    static constexpr int type_id = 81;
+    static constexpr int type_id = 83;
     static constexpr uint8_t opcode = 0;
     VideoNotifyReason reason{};
     uint16_t sequence{};
@@ -279,7 +288,7 @@
   };
 
   struct PortNotifyEvent {
-    static constexpr int type_id = 82;
+    static constexpr int type_id = 84;
     static constexpr uint8_t opcode = 1;
     uint16_t sequence{};
     Time time{};
diff --git a/ui/views/controls/menu/menu_config.h b/ui/views/controls/menu/menu_config.h
index be3fd27..10a4fc6 100644
--- a/ui/views/controls/menu/menu_config.h
+++ b/ui/views/controls/menu/menu_config.h
@@ -193,6 +193,9 @@
   // Shadow elevation of touchable menus.
   int touchable_menu_shadow_elevation = 12;
 
+  // Shadow elevation of touchable submenus.
+  int touchable_submenu_shadow_elevation = 16;
+
   // Vertical padding for touchable menus.
   int vertical_touchable_menu_item_padding = 8;
 
diff --git a/ui/views/controls/menu/menu_controller.cc b/ui/views/controls/menu/menu_controller.cc
index e477fc35..2dbf14bc 100644
--- a/ui/views/controls/menu/menu_controller.cc
+++ b/ui/views/controls/menu/menu_controller.cc
@@ -2503,9 +2503,18 @@
   const MenuConfig& menu_config = MenuConfig::instance();
   // Shadow insets are built into MenuScrollView's preferred size so it must be
   // compensated for when determining the bounds of touchable menus.
+  BubbleBorder::Shadow shadow_type = BubbleBorder::STANDARD_SHADOW;
+  int elevation = menu_config.touchable_menu_shadow_elevation;
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+  if (use_ash_system_ui_layout_) {
+    shadow_type = BubbleBorder::CHROMEOS_SYSTEM_UI_SHADOW;
+    elevation = item->GetParentMenuItem()
+                    ? menu_config.touchable_submenu_shadow_elevation
+                    : menu_config.touchable_menu_shadow_elevation;
+  }
+#endif
   const gfx::Insets border_and_shadow_insets =
-      BubbleBorder::GetBorderAndShadowInsets(
-          menu_config.touchable_menu_shadow_elevation);
+      BubbleBorder::GetBorderAndShadowInsets(elevation, shadow_type);
 
   const gfx::Rect& monitor_bounds = state_.monitor_bounds;
 
diff --git a/ui/views/controls/menu/menu_controller_unittest.cc b/ui/views/controls/menu/menu_controller_unittest.cc
index 56d601e..62f63513 100644
--- a/ui/views/controls/menu/menu_controller_unittest.cc
+++ b/ui/views/controls/menu/menu_controller_unittest.cc
@@ -505,8 +505,7 @@
 
     // Adjust the final bounds to not include the shadow and border.
     const gfx::Insets border_and_shadow_insets =
-        BubbleBorder::GetBorderAndShadowInsets(
-            MenuConfig::instance().touchable_menu_shadow_elevation);
+        GetBorderAndShadowInsets(/*is_submenu=*/false);
     final_bounds.Inset(border_and_shadow_insets);
 
     // Test that the menu will show on screen.
@@ -552,8 +551,7 @@
 
     // Adjust the final bounds to not include the shadow and border.
     const gfx::Insets border_and_shadow_insets =
-        BubbleBorder::GetBorderAndShadowInsets(
-            MenuConfig::instance().touchable_menu_shadow_elevation);
+        GetBorderAndShadowInsets(/*is_submenu=*/false);
     final_bounds.Inset(border_and_shadow_insets);
 
     // Test that the menu is within the monitor bounds.
@@ -603,8 +601,7 @@
 
     // Adjust the final bounds to not include the shadow and border.
     const gfx::Insets border_and_shadow_insets =
-        BubbleBorder::GetBorderAndShadowInsets(
-            MenuConfig::instance().touchable_menu_shadow_elevation);
+        GetBorderAndShadowInsets(/*is_submenu=*/false);
 
     options.anchor_bounds = gfx::Rect(monitor_bounds.origin(), anchor_size);
     gfx::Rect final_bounds = CalculateBubbleMenuBounds(options);
@@ -655,8 +652,7 @@
 
     // Adjust the final bounds to not include the shadow and border.
     const gfx::Insets border_and_shadow_insets =
-        BubbleBorder::GetBorderAndShadowInsets(
-            MenuConfig::instance().touchable_menu_shadow_elevation);
+        GetBorderAndShadowInsets(/*is_submenu=*/true);
 
     MenuItemView* parent_item = item->GetParentMenuItem();
     SubmenuView* sub_menu = parent_item->GetSubmenu();
@@ -878,6 +874,23 @@
     menu_controller_->OpenMenuImpl(parent, true);
   }
 
+  gfx::Insets GetBorderAndShadowInsets(bool is_submenu) {
+    const MenuConfig& menu_config = MenuConfig::instance();
+    int elevation = menu_config.touchable_menu_shadow_elevation;
+    BubbleBorder::Shadow shadow_type = BubbleBorder::STANDARD_SHADOW;
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+    // Increase the submenu shadow elevation and change the shadow style to
+    // ChromeOS system UI shadow style when using Ash System UI layout.
+    if (menu_controller_->use_ash_system_ui_layout()) {
+      if (is_submenu)
+        elevation = menu_config.touchable_submenu_shadow_elevation;
+
+      shadow_type = BubbleBorder::CHROMEOS_SYSTEM_UI_SHADOW;
+    }
+#endif
+    return BubbleBorder::GetBorderAndShadowInsets(elevation, shadow_type);
+  }
+
  private:
   void Init() {
     owner_ = std::make_unique<GestureTestWidget>();
@@ -1063,8 +1076,7 @@
   constexpr gfx::Rect monitor_bounds(0, 0, 500, 500);
   constexpr gfx::Size menu_size(100, 200);
   const gfx::Insets border_and_shadow_insets =
-      BubbleBorder::GetBorderAndShadowInsets(
-          MenuConfig::instance().touchable_menu_shadow_elevation);
+      GetBorderAndShadowInsets(/*is_submenu=*/false);
 
   // Calculate the suitable anchor point to ensure that if the menu shows below
   // the anchor point, the bottom of the menu should be one pixel off the
@@ -1128,8 +1140,7 @@
   constexpr gfx::Rect kMonitorBounds(0, 0, 500, 500);
   constexpr gfx::Size kMenuSize(100, 200);
   const gfx::Insets border_and_shadow_insets =
-      BubbleBorder::GetBorderAndShadowInsets(
-          MenuConfig::instance().touchable_menu_shadow_elevation);
+      GetBorderAndShadowInsets(/*is_submenu=*/false);
 
   // Calculate the suitable anchor point to ensure that if the menu shows below
   // the anchor point, the bottom of the menu should be one pixel off the
diff --git a/ui/views/controls/menu/menu_scroll_view_container.cc b/ui/views/controls/menu/menu_scroll_view_container.cc
index eff2c484..1725c53 100644
--- a/ui/views/controls/menu/menu_scroll_view_container.cc
+++ b/ui/views/controls/menu/menu_scroll_view_container.cc
@@ -385,23 +385,32 @@
 
 void MenuScrollViewContainer::CreateBubbleBorder() {
   const MenuConfig& menu_config = MenuConfig::instance();
-  const int border_radius = menu_config.CornerRadiusForMenu(
-      content_view_->GetMenuItem()->GetMenuController());
+  auto* menu_controller = content_view_->GetMenuItem()->GetMenuController();
+  const int border_radius = menu_config.CornerRadiusForMenu(menu_controller);
+  const bool use_ash_system_ui_layout =
+      menu_controller->use_ash_system_ui_layout();
+
   ui::ColorId id = ui::kColorMenuBackground;
+  BubbleBorder::Shadow shadow_type = BubbleBorder::STANDARD_SHADOW;
 #if BUILDFLAG(IS_CHROMEOS_ASH)
   id = ui::kColorAshSystemUIMenuBackground;
+  // For ash system ui, we use chromeos system ui shadow.
+  if (use_ash_system_ui_layout)
+    shadow_type = BubbleBorder::CHROMEOS_SYSTEM_UI_SHADOW;
 #endif
+
   const SkColor color =
       GetWidget() ? GetColorProvider()->GetColor(id) : gfx::kPlaceholderColor;
-  auto bubble_border = std::make_unique<BubbleBorder>(
-      arrow_, BubbleBorder::STANDARD_SHADOW, color);
-  if (content_view_->GetMenuItem()
-          ->GetMenuController()
-          ->use_ash_system_ui_layout() ||
-      border_radius > 0) {
+  auto bubble_border =
+      std::make_unique<BubbleBorder>(arrow_, shadow_type, color);
+  if (use_ash_system_ui_layout || border_radius > 0) {
     bubble_border->SetCornerRadius(border_radius);
+
+    const bool is_top_menu = !content_view_->GetMenuItem()->GetParentMenuItem();
     bubble_border->set_md_shadow_elevation(
-        menu_config.touchable_menu_shadow_elevation);
+        is_top_menu ? menu_config.touchable_menu_shadow_elevation
+                    : menu_config.touchable_submenu_shadow_elevation);
+
     gfx::Insets insets(menu_config.vertical_touchable_menu_item_padding, 0);
     if (GetFootnote())
       insets.Set(menu_config.vertical_touchable_menu_item_padding, 0, 0, 0);
diff --git a/ui/views/view_unittest.cc b/ui/views/view_unittest.cc
index f643d3f..0c39a2e2 100644
--- a/ui/views/view_unittest.cc
+++ b/ui/views/view_unittest.cc
@@ -1345,19 +1345,11 @@
 namespace {
 
 void RotateCounterclockwise(gfx::Transform* transform) {
-  // clang-format off
-  transform->matrix().set3x3(0, -1, 0,
-                             1,  0, 0,
-                             0,  0, 1);
-  // clang-format on
+  transform->matrix().setRotateAboutZAxisSinCos(-1, 0);
 }
 
 void RotateClockwise(gfx::Transform* transform) {
-  // clang-format off
-  transform->matrix().set3x3( 0, 1, 0,  // NOLINT
-                             -1, 0, 0,
-                              0, 0, 1);
-  // clang-format on
+  transform->matrix().setRotateAboutZAxisSinCos(1, 0);
 }
 
 }  // namespace