910e62b5创建于 1月15日历史提交
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/test/interaction/interactive_browser_window_test.h"

#include <sstream>
#include <string>
#include <utility>
#include <variant>

#include "base/check.h"
#include "base/command_line.h"
#include "base/functional/bind.h"
#include "base/strings/strcat.h"
#include "base/strings/stringprintf.h"
#include "base/strings/to_string.h"
#include "base/task/single_thread_task_runner.h"
#include "base/test/bind.h"
#include "base/test/test_switches.h"
#include "base/values.h"
#include "build/build_config.h"
#include "chrome/browser/ui/browser_navigator.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface_iterator.h"
#include "chrome/browser/ui/interaction/browser_elements.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/interaction/interaction_test_util_browser.h"
#include "chrome/test/interaction/interactive_browser_test_internal.h"
#include "chrome/test/interaction/tracked_element_webcontents.h"
#include "chrome/test/interaction/webcontents_interaction_test_util.h"
#include "components/tabs/public/tab_interface.h"
#include "content/public/browser/visibility.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/update_user_activation_state_interceptor.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/abseil-cpp/absl/functional/overload.h"
#include "third_party/blink/public/mojom/frame/user_activation_notification_type.mojom-shared.h"
#include "third_party/blink/public/mojom/frame/user_activation_update_types.mojom-shared.h"
#include "ui/base/interaction/element_identifier.h"
#include "ui/base/interaction/element_tracker.h"
#include "ui/base/interaction/interaction_sequence.h"
#include "ui/base/interaction/interaction_test_util.h"
#include "ui/base/interaction/interactive_test_internal.h"
#include "ui/base/test/ui_controls.h"

namespace {

// Checks that an element is visible in a non-empty region of the viewport.
// Perhaps look into using checkVisibility() in the future, but this approach
// seems most robust.
constexpr char kElementVisibilityQuery[] =
    R"(
  function(el) {
    const rect = el.getBoundingClientRect();
    const left = Math.max(0, rect.x);
    const top = Math.max(0, rect.y);
    const right = Math.min(rect.x + rect.width, window.innerWidth);
    const bottom = Math.min(rect.y + rect.height, window.innerHeight);
    return right > left && bottom > top;
  }
)";

// Returns the intersection rectangle between a Web UI element `where`, and the
// tracked WebContents `el` it resides in. The web element must reside (at least
// partially) within the web container's bounds and be visible on screen, or
// this will CHECK() fail.
gfx::Rect GetWebElementIntersection(
    ui::TrackedElement* el,
    const WebContentsInteractionTestUtil::DeepQuery& where) {
  auto* const contents = el->AsA<TrackedElementWebContents>();
  CHECK(contents) << "Containing element is not a WebContents";
  const gfx::Rect container_bounds = contents->GetScreenBounds();
  gfx::Rect element_bounds = contents->owner()->GetElementBoundsInScreen(where);
  CHECK(!element_bounds.IsEmpty())
      << "Cannot target DOM element at " << where << " in " << el->identifier()
      << " because its screen bounds are emtpy.";
  gfx::Rect intersect_bounds = element_bounds;
  intersect_bounds.Intersect(container_bounds);
  CHECK(!intersect_bounds.IsEmpty())
      << "Cannot target DOM element at " << where << " in " << el->identifier()
      << " because its screen bounds " << element_bounds.ToString()
      << " are outside the screen bounds of the containing frame, "
      << container_bounds.ToString()
      << ". Did you forget to scroll the element into view? See "
         "ScrollIntoView().";
  return intersect_bounds;
}

// Returns the location of Web UI element `where`, relative to the tracked
// WebContents `el` it resides in. The web element must reside (at least
// partially) within the web container's bounds and be visible on screen, or
// this will CHECK() fail.
gfx::Rect GetRegionInWebContents(
    ui::TrackedElement* el,
    const WebContentsInteractionTestUtil::DeepQuery& where) {
  gfx::Rect intersect_bounds = GetWebElementIntersection(el, where);

  // Compute the sub-region relative to the webcontents.
  auto* const contents = el->AsA<TrackedElementWebContents>();
  CHECK(contents) << "Containing element is not a WebContents";
  const gfx::Rect container_bounds = contents->GetScreenBounds();
  intersect_bounds.Offset(-container_bounds.OffsetFromOrigin());
  return intersect_bounds;
}

// Returns the browser window that `web_contents` is a tab in, or nullptr if it
// is not in a known tab.
BrowserWindowInterface* MaybeGetForWebContentsInTab(
    content::WebContents* web_contents) {
  if (auto* tab = tabs::TabInterface::MaybeGetFromContents(web_contents)) {
    return tab->GetBrowserWindowInterface();
  }
  return nullptr;
}

}  // namespace

DEFINE_CLASS_CUSTOM_ELEMENT_EVENT_TYPE(InteractiveBrowserWindowTestApi,
                                       kDefaultWaitForJsResultEvent);
DEFINE_CLASS_CUSTOM_ELEMENT_EVENT_TYPE(InteractiveBrowserWindowTestApi,
                                       kDefaultWaitForJsResultAtEvent);

InteractiveBrowserWindowTestApi::InteractiveBrowserWindowTestApi()
    : test_impl_(private_test_impl()
                     .MaybeRegisterFrameworkImpl<
                         internal::InteractiveBrowserTestPrivate>()) {}

InteractiveBrowserWindowTestApi::~InteractiveBrowserWindowTestApi() = default;

// static
WebContentsInteractionTestUtil*
InteractiveBrowserWindowTestApi::AsInstrumentedWebContents(
    ui::TrackedElement* el) {
  auto* const web_el = el->AsA<TrackedElementWebContents>();
  CHECK(web_el);
  return web_el->owner();
}

void InteractiveBrowserWindowTestApi::EnableWebUICodeCoverage() {
  test_impl_->MaybeStartWebUICodeCoverage();
}

InteractiveBrowserWindowTestApi::MultiStep
InteractiveBrowserWindowTestApi::ScreenshotWebUi(
    ElementSpecifier element,
    const DeepQuery& where,
    const std::string& screenshot_name,
    const std::string& baseline_cl) {
  StepBuilder builder;
  builder.SetDescription("Compare WebUI Element Screenshot");
  builder.SetElement(element);
  builder.SetStartCallback(base::BindOnce(
      [](InteractiveBrowserWindowTestApi* test, std::string screenshot_name,
         std::string baseline_cl, const DeepQuery& where,
         ui::InteractionSequence* seq, ui::TrackedElement* el) {
        // Locate the element within the bounds of the WebContents.
        const auto window_rect = GetRegionInWebContents(el, where);
        ScreenshotOptions options;
        options.region = window_rect;
        options.focus = ScreenshotFocusMode::kLeaveFocusWhereItIs;
        const auto result = InteractionTestUtilBrowser::CompareScreenshot(
            el, screenshot_name, baseline_cl, options);
        test->private_test_impl().HandleActionResult(seq, el, "Screenshot",
                                                     result);
      },
      base::Unretained(this), screenshot_name, baseline_cl, where));

  auto steps = Steps(MaybeWaitForPaint(element), std::move(builder),
                     MaybeWaitForUserToDismiss(element));
  AddDescriptionPrefix(
      steps, base::StrCat({"ScreenshotWebUi( ", "", screenshot_name, ", ", "",
                           baseline_cl, ""}));
  return steps;
}

InteractiveBrowserWindowTestApi::MultiStep
InteractiveBrowserWindowTestApi::ScreenshotSurface(
    ElementSpecifier element_in_surface,
    const std::string& screenshot_name,
    const std::string& baseline_cl) {
  StepBuilder builder;
  builder.SetDescription("Compare Surface Screenshot");
  builder.SetElement(element_in_surface);
  builder.SetStartCallback(base::BindOnce(
      [](InteractiveBrowserWindowTestApi* test, std::string screenshot_name,
         std::string baseline_cl, ui::InteractionSequence* seq,
         ui::TrackedElement* el) {
        const auto result =
            InteractionTestUtilBrowser::CompareSurfaceScreenshot(
                el, screenshot_name, baseline_cl);
        test->private_test_impl().HandleActionResult(seq, el, "Screenshot",
                                                     result);
      },
      base::Unretained(this), screenshot_name, baseline_cl));

  auto steps = Steps(MaybeWaitForPaint(element_in_surface), std::move(builder),
                     MaybeWaitForUserToDismiss(element_in_surface));
  AddDescriptionPrefix(
      steps, base::StrCat({"ScreenshotSurface( \"", screenshot_name, "\", \"",
                           baseline_cl, "\" )"}));
  return steps;
}

InteractiveBrowserWindowTestApi::MultiStep
InteractiveBrowserWindowTestApi::InstrumentTab(ui::ElementIdentifier id,
                                               std::optional<int> tab_index,
                                               BrowserSpecifier in_browser,
                                               bool wait_for_ready) {
  auto steps = Steps(WithElement(
      ui::test::internal::kInteractiveTestPivotElementId,
      base::BindLambdaForTesting([this, id, tab_index,
                                  in_browser](ui::TrackedElement* el) {
        auto* const browser = GetBrowserWindowFor(el->context(), in_browser);
        CHECK(browser) << "InstrumentTab(): a specific browser is required.";
        test_impl_->AddInstrumentedWebContents(
            WebContentsInteractionTestUtil::ForExistingTabInBrowser(browser, id,
                                                                    tab_index));
      })));
  if (wait_for_ready) {
    steps.push_back(WaitForWebContentsReady(id));
  }
  AddDescriptionPrefix(
      steps,
      base::StringPrintf("InstrumentTab( %s, %d, %d )", id.GetName().c_str(),
                         tab_index.value_or(-1), wait_for_ready));
  return steps;
}

ui::InteractionSequence::StepBuilder
InteractiveBrowserWindowTestApi::InstrumentNextTab(
    ui::ElementIdentifier id,
    BrowserSpecifier in_browser) {
  return WithElement(
             ui::test::internal::kInteractiveTestPivotElementId,
             [this, id, in_browser](ui::TrackedElement* el) {
               auto* const browser =
                   GetBrowserWindowFor(el->context(), in_browser);
               test_impl_->AddInstrumentedWebContents(
                   browser
                       ? WebContentsInteractionTestUtil::ForNextTabInBrowser(
                             browser, id)
                       : WebContentsInteractionTestUtil::ForNextTabInAnyBrowser(
                             id));
             })
      .AddDescriptionPrefix(
          base::StrCat({"InstrumentTab( ", id.GetName(), " )"}));
}

InteractiveBrowserWindowTestApi::MultiStep
InteractiveBrowserWindowTestApi::AddInstrumentedTab(
    ui::ElementIdentifier id,
    GURL url,
    std::optional<int> at_index,
    BrowserSpecifier in_browser) {
  auto steps = Steps(
      InstrumentNextTab(id, in_browser),
      WithElement(
          ui::test::internal::kInteractiveTestPivotElementId,
          base::BindLambdaForTesting([this, url, at_index,
                                      in_browser](ui::TrackedElement* el) {
            auto* const browser =
                GetBrowserWindowFor(el->context(), in_browser);
            CHECK(browser) << "AddInstrumentedTab(): a browser is required.";
            NavigateParams navigate_params(
                browser, url, ui::PageTransition::PAGE_TRANSITION_TYPED);
            navigate_params.tabstrip_index = at_index.value_or(-1);
            navigate_params.disposition =
                WindowOpenDisposition::NEW_FOREGROUND_TAB;
            CHECK(Navigate(&navigate_params));
          })),
      WaitForWebContentsReady(id));
  AddDescriptionPrefix(
      steps, base::StringPrintf("AddInstrumentedTab( %s, %s, %d, )",
                                id.GetName().c_str(), url.spec().c_str(),
                                at_index.value_or(-1)));
  return steps;
}

InteractiveBrowserWindowTestApi::MultiStep
InteractiveBrowserWindowTestApi::InstrumentInnerWebContents(
    ui::ElementIdentifier inner_id,
    ui::ElementIdentifier outer_id,
    size_t inner_contents_index,
    bool wait_for_ready) {
  MultiStep steps;
  steps.emplace_back(Do([this, inner_id, outer_id, inner_contents_index]() {
    test_impl_->AddInstrumentedWebContents(
        WebContentsInteractionTestUtil::ForInnerWebContents(
            outer_id, inner_contents_index, inner_id));
  }));
  if (wait_for_ready) {
    steps.push_back(WaitForWebContentsReady(inner_id));
  }
  AddDescriptionPrefix(
      steps, base::StringPrintf("InstrumentInnerWebContents( %s, %s, %u, %d )",
                                inner_id.GetName(), outer_id.GetName(),
                                inner_contents_index, wait_for_ready));
  return steps;
}

InteractiveBrowserWindowTestApi::StepBuilder
InteractiveBrowserWindowTestApi::UninstrumentWebContents(
    ui::ElementIdentifier id,
    bool fail_if_not_instrumented) {
  return std::move(
      (fail_if_not_instrumented
           ? Check([this, id]() {
               return test_impl_->UninstrumentWebContents(id);
             })
           : Do([this, id]() { test_impl_->UninstrumentWebContents(id); }))
          .SetDescription(
              base::StringPrintf("UninstrumentWebContents(%s)", id.GetName())));
}

// static
ui::InteractionSequence::StepBuilder
InteractiveBrowserWindowTestApi::WaitForWebContentsReady(
    ui::ElementIdentifier webcontents_id,
    std::optional<GURL> expected_url) {
  StepBuilder builder;
  builder.SetDescription(
      base::StringPrintf("WaitForWebContentsReady( %s )",
                         expected_url.value_or(GURL()).spec().c_str()));
  builder.SetElementID(webcontents_id);
  builder.SetContext(kDefaultWebContentsContextMode);
  // Because we're checking the current specific state of the contents, this
  // avoids further navigations breaking the test.
  builder.SetStepStartMode(ui::InteractionSequence::StepStartMode::kImmediate);
  if (expected_url.has_value()) {
    builder.SetStartCallback(base::BindOnce(
        [](GURL expected_url, ui::InteractionSequence* seq,
           ui::TrackedElement* el) {
          auto* const contents =
              el->AsA<TrackedElementWebContents>()->owner()->web_contents();
          if (expected_url != contents->GetURL()) {
            LOG(ERROR) << "Loaded wrong URL; got " << contents->GetURL()
                       << " but expected " << expected_url;
            seq->FailForTesting();
          }
        },
        expected_url.value()));
  }
  return builder;
}

// static
ui::InteractionSequence::StepBuilder
InteractiveBrowserWindowTestApi::WaitForWebContentsNavigation(
    ui::ElementIdentifier webcontents_id,
    std::optional<GURL> expected_url) {
  StepBuilder builder;
  builder.SetDescription(
      base::StringPrintf("WaitForWebContentsNavigation( %s )",
                         expected_url.value_or(GURL()).spec().c_str()));
  builder.SetElementID(webcontents_id);
  builder.SetContext(kDefaultWebContentsContextMode);
  builder.SetTransitionOnlyOnEvent(true);
  if (expected_url.has_value()) {
    builder.SetStartCallback(base::BindOnce(
        [](GURL expected_url, ui::InteractionSequence* seq,
           ui::TrackedElement* el) {
          auto* const contents =
              el->AsA<TrackedElementWebContents>()->owner()->web_contents();
          if (expected_url != contents->GetURL()) {
            LOG(ERROR) << "Loaded wrong URL; got " << contents->GetURL()
                       << " but expected " << expected_url;
            seq->FailForTesting();
          }
        },
        expected_url.value()));
  }
  return builder;
}

// There is a bug that causes WebContents::CompletedFirstVisuallyNonEmptyPaint()
// to occasionally fail to ever become true. This sometimes manifests when
// running tests on Mac builders. In order to prevent tests from hanging when
// trying to ensure a non-empty paint, then, a workaround is required.
//
// See https://crbug.com/332895669 and https://crbug.com/334747109 for more
// information.

namespace {

// Warning message so people aren't surprised when something else in their test
// flakes after this step due to the bug.
constexpr char kPaintWorkaroundWarning[] =
    "\n\nIMPORTANT NOTE FOR TESTERS AND CHROMIUM GARDENERS:\n\n"
    "There is a known issue (crbug.com/332895669, crbug.com/334747109) on Mac "
    "and Win where sometimes WebContents::CompletedFirstVisuallyNonEmptyPaint()"
    " can return false even for a WebContents that is visible and painted, "
    "especially in secondary UI.\n\n"
    "Unfortunately, this has happened. In order to prevent this test from "
    "timing out, we will be ensuring that the page is visible and renders at "
    "least one frame and then continuing the test.\n\n"
    "In most cases, this will only result in a slight delay. However, in a "
    "handful of cases the test may hang or fail because some other code relies "
    "on the page reporting as painted, which we have no direct control over. "
    "If this happens, you may need to disable the test until the lower-level "
    "bug is fixed.\n";

// CheckJsResult() can handle promises, so queue a promise that only succeeds
// after the contents have been rendered.
constexpr char kPaintWorkaroundFunction[] =
    "() => new Promise(resolve => requestAnimationFrame(() => resolve(true)))";

// Event sent on a delay to bypass the "was this WebContents painted?" check on
// platforms where the check is flaky; see comments above.
DEFINE_LOCAL_CUSTOM_ELEMENT_EVENT_TYPE(kPaintWorkaroundEvent);

void MaybePostPaintWorkaroundEvent(ui::TrackedElement* el) {
  // Only secondary web contents are affected.
  if (MaybeGetForWebContentsInTab(
          el->AsA<TrackedElementWebContents>()->owner()->web_contents())) {
    return;
  }
  base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
      FROM_HERE,
      base::BindOnce(
          [](ui::ElementIdentifier id) {
            if (auto* const el = ui::ElementTracker::GetElementTracker()
                                     ->GetElementInAnyContext(id)) {
              ui::ElementTracker::GetFrameworkDelegate()->NotifyCustomEvent(
                  el, kPaintWorkaroundEvent);
            }
          },
          el->identifier()),
      base::Seconds(1));
}

}  // namespace

// static
ui::InteractionSequence::StepBuilder
InteractiveBrowserWindowTestApi::WaitForWebContentsPainted(
    ui::ElementIdentifier webcontents_id) {
  auto wait_step = WaitForEvent(webcontents_id,
                                TrackedElementWebContents::kFirstNonEmptyPaint);
  wait_step.SetContext(kDefaultWebContentsContextMode);
  wait_step.SetMustBeVisibleAtStart(false);
  wait_step.AddDescriptionPrefix("WaitForWebContentsPainted()");

#if BUILDFLAG(IS_MAC) || BUILDFLAG(IS_WIN)
  const bool requires_workaround = true;
#else
  const bool requires_workaround = false;
#endif

  if (requires_workaround) {
    // Workaround for https://crbug.com/332895669 and
    // https://crbug.com/334747109:
    //
    // In parallel with waiting for the WebContents to report as painted, post a
    // delayed event, verify the contents are visible, and ensure at least one
    // frame has rendered. This doesn't fix the problem of the WebContents not
    // reporting as painted, but it does prevent tests that want to ensure that
    // the contents *are* painted from hanging.
    wait_step = AnyOf(
        // Ideally this finishes pretty quickly and we can move on.
        RunSubsequence(std::move(wait_step)),
        // Otherwise, create a timeout after the WebContents is shown.
        RunSubsequence(
            // Ensure that the contents are loaded, then wait a short time.
            InAnyContext(
                AfterShow(webcontents_id, &MaybePostPaintWorkaroundEvent)),
            // After the timeout, first post a verbose warning describing the
            // known issue so that test maintainers are not surprised if
            // something later in the test breaks because paint status is still
            // being reported incorrectly.
            InSameContext(
                AfterEvent(webcontents_id, kPaintWorkaroundEvent,
                           []() { LOG(WARNING) << kPaintWorkaroundWarning; }),
                // Ensure that the WebContents actually believes it's visible.
                CheckElement(
                    webcontents_id,
                    [](ui::TrackedElement* el) {
                      return AsInstrumentedWebContents(el)
                          ->web_contents()
                          ->GetVisibility();
                    },
                    content::Visibility::VISIBLE),
                // Force a frame to render before proceeding.
                // After this is done, we at least known that the contents have
                // been painted - even if the WebContents object itself doesn't!
                CheckJsResult(webcontents_id, kPaintWorkaroundFunction))));
  }

  // If the element is already painted, there is no reason to actually wait (and
  // in fact that will cause a timeout). So only execute the wait step if the
  // WebContents is not ready or not painted.
  //
  // Note: this could also be done with a custom `StateObserver` and
  // `WaitForState()` but this approach requires the fewest steps.
  return IfElement(
             webcontents_id,
             [](const ui::TrackedElement* el) {
               // If the page is not ready (i.e. no element) or not
               // painted, execute the wait step; otherwise skip it.
               return !el || !el->AsA<TrackedElementWebContents>()
                                  ->owner()
                                  ->HasPageBeenPainted();
             },
             Then(std::move(wait_step)))
      .SetContext(kDefaultWebContentsContextMode)
      .AddDescriptionPrefix("WaitForWebContentsPainted()");
}

// static
InteractiveBrowserWindowTestApi::MultiStep
InteractiveBrowserWindowTestApi::NavigateWebContents(
    ui::ElementIdentifier webcontents_id,
    GURL target_url) {
  auto steps = Steps(
      std::move(StepBuilder()
                    .SetDescription("Navigate")
                    .SetElementID(webcontents_id)
                    .SetContext(kDefaultWebContentsContextMode)
                    .SetStartCallback(base::BindOnce(
                        [](GURL url, ui::InteractionSequence* seq,
                           ui::TrackedElement* el) {
                          auto* const owner =
                              el->AsA<TrackedElementWebContents>()->owner();
                          if (url.EqualsIgnoringRef(
                                  owner->web_contents()->GetURL())) {
                            LOG(ERROR) << "Trying to load URL " << url
                                       << " but WebContents URL is already "
                                       << owner->web_contents()->GetURL();
                            seq->FailForTesting();
                          }
                          owner->LoadPage(url);
                        },
                        target_url))),
      WaitForWebContentsNavigation(webcontents_id, target_url));
  AddDescriptionPrefix(
      steps, base::StrCat({"NavigateWebContents( ", target_url.spec(), " )"}));
  return steps;
}

InteractiveBrowserWindowTestApi::MultiStep
InteractiveBrowserWindowTestApi::FocusWebContents(
    ui::ElementIdentifier webcontents_id) {
  auto steps = InAnyContext(WaitForWebContentsPainted(webcontents_id),
                            ActivateSurface(webcontents_id),
                            FocusElement(webcontents_id));
  AddDescriptionPrefix(steps, "FocusWebContents()");
  return steps;
}

// static
InteractiveBrowserWindowTestApi::MultiStep
InteractiveBrowserWindowTestApi::WaitForStateChange(
    ui::ElementIdentifier webcontents_id,
    const StateChange& state_change,
    bool expect_timeout) {
  ui::CustomElementEventType event_type =
      expect_timeout ? state_change.timeout_event : state_change.event;
  CHECK(event_type);
  const bool fail_on_close = !state_change.continue_across_navigation;
  StepBuilder step1;
  step1.SetDescription("Queue Event")
      .SetElementID(webcontents_id)
      .SetContext(kDefaultWebContentsContextMode)
      .SetMustRemainVisible(fail_on_close)
      .SetStartCallback(base::BindOnce(
          [](StateChange state_change, ui::TrackedElement* el) {
            el->AsA<TrackedElementWebContents>()
                ->owner()
                ->SendEventOnStateChange(state_change);
          },
          state_change));
  if (state_change.continue_across_navigation) {
    // This is required to prevent failing if the element would otherwise be
    // hidden due to a navigation between trigger and step start.
    step1.SetStepStartMode(ui::InteractionSequence::StepStartMode::kImmediate);
  }

  auto steps = Steps(
      std::move(step1),
      std::move(StepBuilder()
                    .SetDescription("Wait For Event")
                    .SetElementID(webcontents_id)
                    .SetContext(
                        ui::InteractionSequence::ContextMode::kFromPreviousStep)
                    .SetType(ui::InteractionSequence::StepType::kCustomEvent,
                             event_type)
                    .SetMustBeVisibleAtStart(fail_on_close)));
  AddDescriptionPrefix(
      steps, base::StrCat({"WaitForStateChange( ", base::ToString(state_change),
                           ", ", base::ToString(expect_timeout), " )"}));
  return steps;
}

// static
ui::InteractionSequence::StepBuilder
InteractiveBrowserWindowTestApi::EnsurePresent(
    ui::ElementIdentifier webcontents_id,
    const DeepQuery& where) {
  StepBuilder builder;
  builder.SetDescription(base::StringPrintf(
      "EnsurePresent( %s, %s )", webcontents_id.GetName().c_str(),
      internal::InteractiveBrowserTestPrivate::DeepQueryToString(where)
          .c_str()));
  builder.SetElementID(webcontents_id);
  builder.SetContext(kDefaultWebContentsContextMode);
  builder.SetStartCallback(base::BindOnce(
      [](DeepQuery where, ui::InteractionSequence* seq,
         ui::TrackedElement* el) {
        if (!AsInstrumentedWebContents(el)->Exists(where)) {
          LOG(ERROR) << "Expected DOM element to be present: " << where;
          seq->FailForTesting();
        }
      },
      where));
  return builder;
}

// static
ui::InteractionSequence::StepBuilder
InteractiveBrowserWindowTestApi::EnsureNotPresent(
    ui::ElementIdentifier webcontents_id,
    const DeepQuery& where) {
  StepBuilder builder;
  builder.SetDescription(base::StringPrintf(
      "EnsureNotPresent( %s, %s )", webcontents_id.GetName().c_str(),
      internal::InteractiveBrowserTestPrivate::DeepQueryToString(where)
          .c_str()));
  builder.SetElementID(webcontents_id);
  builder.SetContext(kDefaultWebContentsContextMode);
  builder.SetStartCallback(base::BindOnce(
      [](DeepQuery where, ui::InteractionSequence* seq,
         ui::TrackedElement* el) {
        if (AsInstrumentedWebContents(el)->Exists(where)) {
          LOG(ERROR) << "Expected DOM element not to be present: " << where;
          seq->FailForTesting();
        }
      },
      where));
  return builder;
}

ui::InteractionSequence::StepBuilder
InteractiveBrowserWindowTestApi::EnsureNotVisible(
    ui::ElementIdentifier webcontents_id,
    const DeepQuery& where) {
  return IfElement(
      webcontents_id,
      [where](const ui::TrackedElement* el) {
        return el->AsA<TrackedElementWebContents>()->owner()->Exists(where);
      },
      Then(CheckJsResultAt(webcontents_id, where, kElementVisibilityQuery,
                           false)));
}

// static
ui::InteractionSequence::StepBuilder InteractiveBrowserWindowTestApi::ExecuteJs(
    ui::ElementIdentifier webcontents_id,
    const std::string& function,
    ExecuteJsMode mode) {
  StepBuilder builder;
  builder.SetDescription(
      base::StringPrintf("ExecuteJs(\"\n%s\n\")", function.c_str()));
  builder.SetElementID(webcontents_id);
  builder.SetContext(kDefaultWebContentsContextMode);
  switch (mode) {
    case ExecuteJsMode::kFireAndForget:
      builder.SetMustRemainVisible(false);
      builder.SetStartCallback(base::BindOnce(
          [](std::string function, ui::TrackedElement* el) {
            AsInstrumentedWebContents(el)->Execute(function);
          },
          function));
      break;
    case ExecuteJsMode::kWaitForCompletion:
      builder.SetStartCallback(base::BindOnce(
          [](std::string function, ui::InteractionSequence* seq,
             ui::TrackedElement* el) {
            const auto full_function = base::StringPrintf(
                "() => { (%s)(); return false; }", function.c_str());
            std::string error_msg;
            AsInstrumentedWebContents(el)->Evaluate(full_function, &error_msg);
            if (!error_msg.empty()) {
              LOG(ERROR) << "ExecuteJsAt() failed: " << error_msg;
              seq->FailForTesting();
            }
          },
          function));
      break;
  }
  return builder;
}

// static
ui::InteractionSequence::StepBuilder
InteractiveBrowserWindowTestApi::ExecuteJsAt(
    ui::ElementIdentifier webcontents_id,
    const DeepQuery& where,
    const std::string& function,
    ExecuteJsMode mode) {
  StepBuilder builder;
  builder.SetDescription(base::StringPrintf(
      "ExecuteJsAt( %s, \"\n%s\n\")",
      internal::InteractiveBrowserTestPrivate::DeepQueryToString(where).c_str(),
      function.c_str()));
  builder.SetElementID(webcontents_id);
  builder.SetContext(kDefaultWebContentsContextMode);
  switch (mode) {
    case ExecuteJsMode::kFireAndForget:
      builder.SetMustRemainVisible(false);
      builder.SetStartCallback(base::BindOnce(
          [](DeepQuery where, std::string function, ui::TrackedElement* el) {
            AsInstrumentedWebContents(el)->ExecuteAt(where, function);
          },
          where, function));
      break;
    case ExecuteJsMode::kWaitForCompletion:
      builder.SetStartCallback(base::BindOnce(
          [](DeepQuery where, std::string function,
             ui::InteractionSequence* seq, ui::TrackedElement* el) {
            const auto full_function = base::StringPrintf(
                R"(
              (el, err) => {
                if (err) {
                  throw err;
                }
                (%s)(el);
                return false;
              }
            )",
                function.c_str());
            std::string error_msg;
            AsInstrumentedWebContents(el)->EvaluateAt(where, full_function,
                                                      &error_msg);
            if (!error_msg.empty()) {
              LOG(ERROR) << "ExecuteJsAt() failed: " << error_msg;
              seq->FailForTesting();
            }
          },
          where, function));
      break;
  }
  return builder;
}

// static
ui::InteractionSequence::StepBuilder
InteractiveBrowserWindowTestApi::CheckJsResult(
    ui::ElementIdentifier webcontents_id,
    const std::string& function) {
  return CheckJsResult(webcontents_id, function, internal::IsTruthyMatcher());
}

// static
ui::InteractionSequence::StepBuilder
InteractiveBrowserWindowTestApi::CheckJsResultAt(
    ui::ElementIdentifier webcontents_id,
    const DeepQuery& where,
    const std::string& function) {
  return CheckJsResultAt(webcontents_id, where, function,
                         internal::IsTruthyMatcher());
}

InteractiveBrowserWindowTestApi::MultiStep
InteractiveBrowserWindowTestApi::WaitForJsResult(
    ui::ElementIdentifier webcontents_id,
    const std::string& function) {
  return WaitForJsResult(webcontents_id, function, IsTruthy());
}

InteractiveBrowserWindowTestApi::MultiStep
InteractiveBrowserWindowTestApi::WaitForJsResultAt(
    ui::ElementIdentifier webcontents_id,
    const DeepQuery& where,
    const std::string& function) {
  return WaitForJsResultAt(webcontents_id, where, function, IsTruthy());
}

ui::InteractionSequence::StepBuilder
InteractiveBrowserWindowTestApi::ScrollIntoView(
    ui::ElementIdentifier web_contents,
    const DeepQuery& where) {
  return std::move(
      ExecuteJsAt(web_contents, where,
                  "(el) => { el.scrollIntoView({ behavior: 'instant' }); }")
          .SetDescription("ScrollIntoView()"));
}

InteractiveBrowserWindowTestApi::MultiStep
InteractiveBrowserWindowTestApi::WaitForElementVisible(
    ui::ElementIdentifier web_contents,
    const DeepQuery& where) {
  DEFINE_LOCAL_CUSTOM_ELEMENT_EVENT_TYPE(kWaitforElementVisibleCompleteEvent);

  StateChange change;
  change.event = kWaitforElementVisibleCompleteEvent;
  change.test_function = kElementVisibilityQuery;
  change.type = StateChange::Type::kExistsAndConditionTrue;
  change.where = where;

  auto steps = WaitForStateChange(web_contents, change);
  AddDescriptionPrefix(steps, "WaitForElementVisible()");
  return steps;
}

ui::InteractionSequence::StepBuilder
InteractiveBrowserWindowTestApi::ClickElement(
    ui::ElementIdentifier web_contents,
    const DeepQuery& where,
    ui_controls::MouseButton button,
    ui_controls::AcceleratorState modifiers,
    ExecuteJsMode execute_mode) {
  int js_button;
  switch (button) {
    case ui_controls::LEFT:
      js_button = 0;
      break;
    case ui_controls::MIDDLE:
      js_button = 1;
      break;
    case ui_controls::RIGHT:
      js_button = 2;
      break;
  }

  const bool shift = modifiers & ui_controls::kShift;
  const bool alt = modifiers & ui_controls::kAlt;
  const bool ctrl = modifiers & ui_controls::kControl;
  const bool meta = modifiers & ui_controls::kCommand;

  auto b2s = [](bool b) { return base::ToString(b); };

  const std::string command = base::StringPrintf(
      R"(
      function(el) {
        const rect = el.getBoundingClientRect();
        const left = Math.max(0, rect.x);
        const top = Math.max(0, rect.y);
        const right = Math.min(rect.x + rect.width, window.innerWidth);
        const bottom = Math.min(rect.y + rect.height, window.innerHeight);
        if (right <= left || bottom <= top) {
          throw new Error(
              'Target element is zero size or ' +
              'has empty intersection with the viewport.');
        }
        const x = (left + right) / 2;
        const y = (top + bottom) / 2;

        const event = new MouseEvent(
            'click',
            {
              bubbles: true,
              cancelable: true,
              clientX: x,
              clientY: y,
              button: %d,
              shiftKey: %s,
              altKey: %s,
              ctrlKey: %s,
              metaKey: %s
            }
        );
        el.dispatchEvent(event);
      }
    )",
      js_button, b2s(shift), b2s(alt), b2s(ctrl), b2s(meta));

  return ExecuteJsAt(web_contents, where, command, execute_mode)
      .SetDescription("ClickElement()");
}

// static
InteractiveBrowserWindowTestApi::RelativePositionCallback
InteractiveBrowserWindowTestApi::DeepQueryToRelativePosition(
    const DeepQuery& where) {
  return base::BindOnce(
      [](DeepQuery q, ui::TrackedElement* el) {
        gfx::Rect intersect_bounds = GetWebElementIntersection(el, q);
        return intersect_bounds.CenterPoint();
      },
      where);
}

InteractiveBrowserWindowTestApi::MultiStep
InteractiveBrowserWindowTestApi::MaybeWaitForPaint(ElementSpecifier element) {
  // Only wait if `element` is actually a `WebContents`.
  //
  // WebContents are typically only referred to via their assigned IDs.
  // TODO(dfried): possibly handle (rare) cases where a name has been assigned.
  if (!element.is_identifier()) {
    return MultiStep();
  }
  const auto element_id = element.identifier();

  // Do a `WaitForWebContentsPainted()`, but only if the ID has been assigned to
  // an instrumented `WebContents`.
  return Steps(If(
      [this, element_id]() {
        return test_impl_->IsInstrumentedWebContents(element_id);
      },
      Then(WaitForWebContentsPainted(element_id))));
}

// static
InteractiveBrowserWindowTestApi::StepBuilder
InteractiveBrowserWindowTestApi::MaybeWaitForUserToDismiss(
    ElementSpecifier element) {
  // In interactive mode (--test-launcher-interactive) the behavior for pixel
  // tests is to wait until the user closes/hides the surface that has the
  // element to screenshot. This may break the rest of the test, but it's fine
  // because the purpose of interactive mode is for a human user to observe
  // what the test sees during the screenshot step.
  return If(
      []() {
        return base::CommandLine::ForCurrentProcess()->HasSwitch(
            switches::kTestLauncherInteractive);
      },
      Then(Log(
               R"(

------------------

Since --test-launcher-interactive is specified, this test will now wait for you
to dismiss the element that is being screenshot:

)",
               "  ", element,
               R"(

Note that This may cause the remainder of the test to fail or crash, if the test
does not expect the surface to be dismissed.

------------------
)"),
           WaitForHide(element)));
}

BrowserWindowInterface* InteractiveBrowserWindowTestApi::GetBrowserWindowFor(
    ui::ElementContext current_context,
    BrowserSpecifier spec) {
  return std::visit(
      absl::Overload{
          [](AnyBrowser) -> BrowserWindowInterface* { return nullptr; },
          [current_context](CurrentBrowser) {
            BrowserWindowInterface* const browser =
                InteractionTestUtilBrowser::GetBrowserFromContext(
                    current_context);
            CHECK(browser) << "Current context is not a browser window.";
            return browser;
          },
          [](BrowserWindowInterface* browser) {
            CHECK(browser) << "BrowserSpecifier: browser window is null.";
            return browser;
          },
          [](std::reference_wrapper<BrowserWindowInterface*> browser) {
            CHECK(browser.get()) << "BrowserSpecifier: browser window is null.";
            return browser.get();
          }},
      spec);
}