File: interactive_browser_test.h

package info (click to toggle)
chromium 138.0.7204.157-1
  • links: PTS, VCS
  • area: main
  • in suites: trixie
  • size: 6,071,864 kB
  • sloc: cpp: 34,936,859; ansic: 7,176,967; javascript: 4,110,704; python: 1,419,953; asm: 946,768; xml: 739,967; pascal: 187,324; sh: 89,623; perl: 88,663; objc: 79,944; sql: 50,304; cs: 41,786; fortran: 24,137; makefile: 21,806; php: 13,980; tcl: 13,166; yacc: 8,925; ruby: 7,485; awk: 3,720; lisp: 3,096; lex: 1,327; ada: 727; jsp: 228; sed: 36
file content (733 lines) | stat: -rw-r--r-- 31,466 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#ifndef CHROME_TEST_INTERACTION_INTERACTIVE_BROWSER_TEST_H_
#define CHROME_TEST_INTERACTION_INTERACTIVE_BROWSER_TEST_H_

#include <concepts>
#include <functional>
#include <memory>
#include <optional>
#include <sstream>
#include <type_traits>
#include <utility>
#include <variant>
#include <vector>

#include "base/functional/callback_helpers.h"
#include "base/test/bind.h"
#include "base/test/rectify_callback.h"
#include "build/build_config.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/views/frame/browser_view.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/webcontents_interaction_test_util.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/base/interaction/element_identifier.h"
#include "ui/base/interaction/element_test_util.h"
#include "ui/base/interaction/element_tracker.h"
#include "ui/base/interaction/expect_call_in_scope.h"
#include "ui/base/interaction/interaction_sequence.h"
#include "ui/base/interaction/interaction_test_util.h"
#include "ui/base/interaction/interactive_test_definitions.h"
#include "ui/base/test/ui_controls.h"
#include "ui/views/interaction/element_tracker_views.h"
#include "ui/views/interaction/interactive_views_test.h"
#include "ui/views/view.h"
#include "ui/views/view_utils.h"
#include "url/gurl.h"

namespace ui {
class TrackedElement;
}

class Browser;

// Provides interactive test functionality for Views.
//
// Interactive tests use InteractionSequence, ElementTracker, and
// InteractionTestUtil to provide a common library of concise test methods. This
// convenience API is nicknamed "Kombucha" (see README.md for more information).
//
// This class is not a test fixture; it is a mixin that can be added to an
// existing browser test class using `InteractiveBrowserTestT<T>` - or just use
// `InteractiveBrowserTest`, which *is* a test fixture (preferred; see below).
class InteractiveBrowserTestApi : public views::test::InteractiveViewsTestApi {
 public:
  InteractiveBrowserTestApi();
  ~InteractiveBrowserTestApi() override;

  using DeepQuery = WebContentsInteractionTestUtil::DeepQuery;
  using StateChange = WebContentsInteractionTestUtil::StateChange;

  // Since we enforce a 1:1 correspondence between ElementIdentifiers and
  // WebContents defaulting to ContextMode::kAny prevents accidentally missing
  // the correct context, which is a common mistake that causes tests to
  // mysteriously time out looking in the wrong place.
  static constexpr ui::InteractionSequence::ContextMode
      kDefaultWebContentsContextMode =
          ui::InteractionSequence::ContextMode::kAny;

  // Shorthand to convert a tracked element into a instrumented WebContents.
  // The element should be a TrackedElementWebContents.
  static WebContentsInteractionTestUtil* AsInstrumentedWebContents(
      ui::TrackedElement* el);

  // Manually enable WebUI code coverage (slightly experimental). Call during
  // `SetUpOnMainThread()` or in your test body before `RunTestSequence()`.
  //
  // Has no effect if the `--devtools-code-coverage` command line flag isn't
  // set. This will cause tests to run longer (possibly timing out) and is not
  // compatible with all WebUI pages. Use liberally, but at your own risk.
  //
  // TODO(b/273545898, b/273290598): when coverage is more robust, make this
  // automatic for all tests that touch WebUI.
  void EnableWebUICodeCoverage();

  // Takes a screenshot of the specified element, with name `screenshot_name`
  // (may be empty for tests that take only one screenshot) and `baseline_cl`,
  // which should be set to match the CL number when a screenshot should change.
  //
  // Currently, is somewhat unreliable for WebUI embedded in bubbles or dialogs
  // (e.g. Tab Search dropdown) but should work fairly well in most other cases.
  [[nodiscard]] MultiStep Screenshot(ElementSpecifier element,
                                     const std::string& screenshot_name,
                                     const std::string& baseline_cl);

  // As `Screenshot()` but takes a screenshot of the entire surface (widget,
  // WebUI, etc.) containing `element_in_surface`. See `Screenshot()` for more
  // information.
  [[nodiscard]] MultiStep ScreenshotSurface(ElementSpecifier element_in_surface,
                                            const std::string& screenshot_name,
                                            const std::string& baseline_cl);

  struct CurrentBrowser {};
  struct AnyBrowser {};

  // Specifies which browser to use when instrumenting a tab.
  using BrowserSpecifier = std::variant<
      // Use the browser associated with the context of the current test step;
      // if unspecified use the default context for the sequence.
      CurrentBrowser,
      // Find a tab in any browser.
      AnyBrowser,
      // Specify a browser that is known at the time the sequence is created.
      // The browser must persist until the step executes.
      Browser*,
      // Specify a browser pointer that will be valid by the time the step
      // executes. Use std::ref() to wrap the pointer that will receive the
      // value.
      std::reference_wrapper<Browser*>>;

  // Instruments tab `tab_index` in `in_browser` as `id`. If `tab_index` is
  // unspecified, the active tab is used.
  //
  // Does not support AnyBrowser; you must specify a browser.
  //
  // If `wait_for_ready` is true (default), the step will not complete until the
  // current page in the WebContents is fully loaded.
  [[nodiscard]] MultiStep InstrumentTab(
      ui::ElementIdentifier id,
      std::optional<int> tab_index = std::nullopt,
      BrowserSpecifier in_browser = CurrentBrowser(),
      bool wait_for_ready = true);

  // Instruments the next tab in `in_browser` as `id`.
  [[nodiscard]] StepBuilder InstrumentNextTab(
      ui::ElementIdentifier id,
      BrowserSpecifier in_browser = CurrentBrowser());

  // Opens a new tab for `url` and instruments it as `id`. The tab is inserted
  // at `at_index` if specified, otherwise the browser decides.
  //
  // Does not support AnyBrowser; you must specify a browser.
  [[nodiscard]] MultiStep AddInstrumentedTab(
      ui::ElementIdentifier id,
      GURL url,
      std::optional<int> tab_index = std::nullopt,
      BrowserSpecifier in_browser = CurrentBrowser());

  // Instruments the WebContents held by `web_view` as `id`. Will wait for the
  // WebView to become visible if it is not.
  //
  // If `wait_for_ready` is true (default), the step will not complete until the
  // current page in the WebContents is fully loaded. (Note that this may not
  // cover dynamic loading of data; you may need to do a WaitForStateChange() to
  // be sure dynamic content is loaded).
  [[nodiscard]] MultiStep InstrumentNonTabWebView(ui::ElementIdentifier id,
                                                  ElementSpecifier web_view,
                                                  bool wait_for_ready = true);
  [[nodiscard]] MultiStep InstrumentNonTabWebView(
      ui::ElementIdentifier id,
      AbsoluteViewSpecifier web_view,
      bool wait_for_ready = true);

  // Instruments an inner webview of an already-instrumented WebContents of any
  // type; the resulting element will be present only when the parent element is
  // *and* the inner webview is loaded and ready.
  //
  // Do not use with iframe; just instrument the primary contents and use
  // `DeepQuery` instead.
  [[nodiscard]] MultiStep InstrumentInnerWebContents(
      ui::ElementIdentifier inner_id,
      ui::ElementIdentifier outer_id,
      size_t inner_contents_index,
      bool wait_for_ready = true);

  // Removes instrumentation for the WebContents with identifier `id`.
  // `fail_if_not_instrumented` defines what happens if `id` is not in use;
  // if true, crashes the test. If false, ignores and continues.
  [[nodiscard]] StepBuilder UninstrumentWebContents(
      ui::ElementIdentifier id,
      bool fail_if_not_instrumented = true);

  // These convenience methods wait for page navigation/ready. If you specify
  // `expected_url`, the test will fail if that is not the loaded page. If you
  // do not, there is no step start callback and you can add your own logic.
  //
  // Note that because `webcontents_id` is expected to be globally unique, these
  // actions have SetFindElementInAnyContext(true) by default (otherwise it's
  // really easy to forget to add InAnyContext() and have your test not work.
  [[nodiscard]] static StepBuilder WaitForWebContentsReady(
      ui::ElementIdentifier webcontents_id,
      std::optional<GURL> expected_url = std::nullopt);
  [[nodiscard]] static StepBuilder WaitForWebContentsNavigation(
      ui::ElementIdentifier webcontents_id,
      std::optional<GURL> expected_url = std::nullopt);

  // Waits for the instrumented WebContents with the given `webcontents_id` to
  // be painted at least once. If a WebContents is not painted when some events
  // are being sent, they may not be routed correctly. Likewise, a screenshot of
  // a WebContents won't be guaranteed to have any content without being painted
  // first. Because paint often happens quickly, this can lead to tests that
  // mostly pass but cause flakes due to hidden race conditions on slower
  // machines and test-bots.
  //
  // Note: waiting for contentful paint isn't automatic when instrumenting,
  // specifically because not all pages actually paint - empty pages, background
  // pages, and pages loaded in WebViews that are hidden or zero size do not
  // paint. They are also not required for all interactions. Where possible,
  // verbs provided in this API that do require at least one paint will include
  // this verb conditionally to avoid problems.
  [[nodiscard]] static StepBuilder WaitForWebContentsPainted(
      ui::ElementIdentifier webcontents_id);

  // This convenience method navigates the page at `webcontents_id` to
  // `new_url`, which must be different than its current URL. The sequence will
  // not proceed until navigation completes, and will fail if the wrong URL is
  // loaded.
  [[nodiscard]] static MultiStep NavigateWebContents(
      ui::ElementIdentifier webcontents_id,
      GURL new_url);

  // Raises the surface containing `webcontents_id` and focuses the WebContents
  // as if a user had interacted directly with it. This is useful if you want
  // the WebContents to e.g. respond to accelerators.
  //
  // Note that this is shorthand for:
  //  - WaitForWebContentsPainted(webcontents_id)
  //  - ActivateSurface(webcontents_id)
  //  - FocusElement(webcontents_id)
  //
  // The last is there to prevent any input from being swallowed before it is
  // sent to the contents.
  [[nodiscard]] MultiStep FocusWebContents(
      ui::ElementIdentifier webcontents_id);

  // Waits for the given `state_change` in `webcontents_id`. The sequence will
  // fail if the change times out, unless `expect_timeout` is true, in which
  // case the StateChange *must* timeout, and |state_change.timeout_event| must
  // be set.
  //
  // Generally, you are better off using WaitForJsResult[At] instead of a raw
  // WaitForStateChange.
  [[nodiscard]] static MultiStep WaitForStateChange(
      ui::ElementIdentifier webcontents_id,
      const StateChange& state_change,
      bool expect_timeout = false);

  // Required to keep from hiding inherited versions of these methods.
  using InteractiveViewsTestApi::EnsureNotPresent;
  using InteractiveViewsTestApi::EnsurePresent;

  // Ensures that there is an element at path `where` in `webcontents_id`.
  // Unlike InteractiveTestApi::EnsurePresent, this verb can be inside an
  // InAnyContext() block.
  [[nodiscard]] static StepBuilder EnsurePresent(
      ui::ElementIdentifier webcontents_id,
      const DeepQuery& where);

  // Ensures that there is no element at path `where` in `webcontents_id`.
  // Unlike InteractiveTestApi::EnsurePresent, this verb can be inside an
  // InAnyContext() block.
  [[nodiscard]] static StepBuilder EnsureNotPresent(
      ui::ElementIdentifier webcontents_id,
      const DeepQuery& where);

  // How to execute JavaScript when calling ExecuteJs() and ExecuteJsAt().
  enum class ExecuteJsMode {
    // Ensures that the code sent to the renderer completes without error before
    // the next step can proceed. If an error occurs, the test fails.
    //
    // This is the default.
    kWaitForCompletion,
    // Sends the code to the renderer for execution, but does not wait for a
    // response. If an error occurs, it may appear in the log, but the test
    // will not detect it and will not fail.
    //
    // Use this mode if the code you are injecting will prevent the renderer
    // from communicating the result back to the browser process.
    kFireAndForget,
  };

  // Execute javascript `function`, which should take no arguments, in
  // WebContents `webcontents_id`.
  //
  // You can use this method to call an existing function with no arguments in
  // the global scope; to do that, specify only the name of the method (e.g.
  // `myMethod` rather than `myMethod()`).
  [[nodiscard]] static StepBuilder ExecuteJs(
      ui::ElementIdentifier webcontents_id,
      const std::string& function,
      ExecuteJsMode mode = ExecuteJsMode::kWaitForCompletion);

  // Execute javascript `function`, which should take a single DOM element as an
  // argument, with the element at `where`, in WebContents `webcontents_id`.
  [[nodiscard]] static StepBuilder ExecuteJsAt(
      ui::ElementIdentifier webcontents_id,
      const DeepQuery& where,
      const std::string& function,
      ExecuteJsMode mode = ExecuteJsMode::kWaitForCompletion);

  // Returns a matcher that matches truthy values.
  //
  // Use this if you don't want to compare specifically to "true", but just want
  // to know that a value isn't null/false/empty/zero.
  [[nodiscard]] static auto IsTruthy() { return internal::IsTruthyMatcher(); }

  // Executes javascript `function`, which should take no arguments and return a
  // value, in WebContents `webcontents_id`, and fails if the result is not
  // truthy.
  //
  // If `function` instead returns a promise, the result of the promise is
  // evaluated for truthiness. If the promise rejects, CheckJsResult() fails.
  [[nodiscard]] static StepBuilder CheckJsResult(
      ui::ElementIdentifier webcontents_id,
      const std::string& function);

  // Executes javascript `function`, which should take no arguments and return a
  // value, in WebContents `webcontents_id`, and fails if the result does not
  // match `matcher`, which can be a literal or a testing::Matcher.
  //
  // Note that only the following types are supported:
  //  - string (for literals, you may pass a const char*)
  //  - bool
  //  - int
  //  - double (will also match integer return values)
  //  - base::Value (required if you want to match a list or dictionary)
  //
  // You must pass a literal or Matcher that matches the type returned by the
  // javascript function. If your function could return either an integer or a
  // floating-point value, you *must* use a double.
  //
  // If `function` instead returns a promise, the result of the promise is
  // evaluated against `matcher`. If the promise rejects, CheckJsResult() fails.
  template <typename T>
  [[nodiscard]] static StepBuilder CheckJsResult(
      ui::ElementIdentifier webcontents_id,
      const std::string& function,
      T&& matcher);

  // Executes javascript `function`, which should take a single DOM element as
  // an argument and returns a value, in WebContents `webcontents_id` on the
  // element specified by `where`, and fails if the result is not truthy.
  //
  // If `function` instead returns a promise, the result of the promise is
  // evaluated for truthiness. If the promise rejects, CheckJsResultAt() fails.
  [[nodiscard]] static StepBuilder CheckJsResultAt(
      ui::ElementIdentifier webcontents_id,
      const DeepQuery& where,
      const std::string& function);

  // Executes javascript `function`, which should take a single DOM element as
  // an argument and returns a value, in WebContents `webcontents_id` on the
  // element specified by `where`, and fails if the result does not match
  // `matcher`, which can be a literal or a testing::Matcher.
  //
  // If `function` instead returns a promise, the result of the promise is
  // evaluated against `matcher`. If the promise rejects, CheckJsResultAt()
  // fails.
  //
  // See notes on CheckJsResult() for what values and Matchers are supported.
  template <typename T>
  [[nodiscard]] static StepBuilder CheckJsResultAt(
      ui::ElementIdentifier webcontents_id,
      const DeepQuery& where,
      const std::string& function,
      T&& matcher);

  DECLARE_CLASS_CUSTOM_ELEMENT_EVENT_TYPE(kDefaultWaitForJsResultEvent);
  DECLARE_CLASS_CUSTOM_ELEMENT_EVENT_TYPE(kDefaultWaitForJsResultAtEvent);

  // Polls `webcontents_id` until the result of `function` matches `value`.
  //
  // It is not necessary to specify `event` unless you are waiting for results
  // in parallel (then each event must be unique).
  template <typename M>
  [[nodiscard]] MultiStep WaitForJsResult(
      ui::ElementIdentifier webcontents_id,
      const std::string& function,
      M&& value,
      bool continue_across_navigation = false,
      ui::CustomElementEventType event = kDefaultWaitForJsResultEvent);

  // Polls element at `where` in `webcontents_id` until the element exists and
  // the result of calling `function` on it matches `value`.
  //
  // It is not necessary to specify `event` unless you are waiting for results
  // in parallel (then each event must be unique).
  template <typename M>
  [[nodiscard]] MultiStep WaitForJsResultAt(
      ui::ElementIdentifier webcontents_id,
      const DeepQuery& where,
      const std::string& function,
      M&& value,
      bool element_must_be_present_at_start = false,
      bool continue_across_navigation = false,
      ui::CustomElementEventType event = kDefaultWaitForJsResultAtEvent);

  // Polls `webcontents_id` until the result of `function` is truthy.
  //
  // Equivalent to `WaitForJsResult(webcontents_id, function, IsTruthy())`.
  [[nodiscard]] MultiStep WaitForJsResult(ui::ElementIdentifier webcontents_id,
                                          const std::string& function);

  // Polls element at `where` in `webcontents_id` until the element exists and
  // the result of calling `function` on it is truthy.
  //
  // Equivalent to:
  // `WaitForJsResultAt(webcontents_id, where function, IsTruthy())`.
  [[nodiscard]] MultiStep WaitForJsResultAt(
      ui::ElementIdentifier webcontents_id,
      const DeepQuery& where,
      const std::string& function);

  // These are required so the following overloads don't hide the base class
  // variations.
  using InteractiveViewsTestApi::DragMouseTo;
  using InteractiveViewsTestApi::MoveMouseTo;

  // Find the DOM element at the given path in the reference element, which
  // should be an instrumented WebContents; see Instrument*(). Move the mouse to
  // the element's center point in screen coordinates.
  //
  // If the DOM element may be scrolled outside of the current viewport,
  // consider using ScrollIntoView(web_contents, where) before this verb.
  [[nodiscard]] MultiStep MoveMouseTo(ui::ElementIdentifier web_contents,
                                      const DeepQuery& where);

  // Find the DOM element at the given path in the reference element, which
  // should be an instrumented WebContents; see Instrument*(). Perform a drag
  // from the mouse's current location to the element's center point in screen
  // coordinates, and then if `release` is true, releases the mouse button.
  //
  // If the DOM element may be scrolled outside of the current viewport,
  // consider using ScrollIntoView(web_contents, where) before this verb.
  [[nodiscard]] MultiStep DragMouseTo(ui::ElementIdentifier web_contents,
                                      const DeepQuery& where,
                                      bool release = true);

  using InteractiveViewsTestApi::ScrollIntoView;

  // Scrolls the DOM element at `where` in instrumented WebContents
  // `web_contents` into view; see Instrument*(). The scrolling happens
  // instantaneously, without animation, and should be available on the next
  // render frame or call into the renderer.
  [[nodiscard]] StepBuilder ScrollIntoView(ui::ElementIdentifier web_contents,
                                           const DeepQuery& where);

  // Waits until the intersection of the element's bounds and the window bounds
  // are nonempty.
  [[nodiscard]] MultiStep WaitForElementVisible(
      ui::ElementIdentifier web_contents,
      const DeepQuery& where);

  // Simulates clicking on an HTML element by injecting the click event directly
  // into the DOM. You can specify the mouse button and additional modifier
  // keys (default is left-click, no modifiers).
  //
  // Normally, clicking with buttons other than the left mouse button generates
  // an auxclick event rather than a click event. However, injecting auxclick
  // does not e.g. trigger navigation when clicking a link, so in all these
  // cases, vanilla click events are sent, which should be handled normally for
  // backwards-compatibility reasons.
  //
  // Note that if your WebUI will close in response to this action, you should
  // set `execute_mode` to `kFireAndForget` to avoid failure to receive
  // confirmation.
  [[nodiscard]] StepBuilder ClickElement(
      ui::ElementIdentifier web_contents,
      const DeepQuery& where,
      ui_controls::MouseButton button = ui_controls::LEFT,
      ui_controls::AcceleratorState modifiers = ui_controls::kNoAccelerator,
      ExecuteJsMode execute_mode = ExecuteJsMode::kWaitForCompletion);

  // Convenience version of `ClickElement()` (see above) for
  // default-left-clicking when you need to specify the execution mode.
  [[nodiscard]] inline StepBuilder ClickElement(
      ui::ElementIdentifier web_contents,
      const DeepQuery& where,
      ExecuteJsMode execute_mode) {
    return ClickElement(web_contents, where, ui_controls::LEFT,
                        ui_controls::kNoAccelerator, execute_mode);
  }

 protected:
  explicit InteractiveBrowserTestApi(
      std::unique_ptr<internal::InteractiveBrowserTestPrivate>
          private_test_impl);

 private:
  // Common logic for WaitForJsResult[At].
  template <typename M>
  [[nodiscard]] MultiStep WaitForJsResultCommon(
      ui::ElementIdentifier webcontents_id,
      StateChange::Type type,
      const std::string& function,
      const DeepQuery& where,
      M&& value,
      bool continue_across_navigation,
      ui::CustomElementEventType event);

  static RelativePositionCallback DeepQueryToRelativePosition(
      const DeepQuery& query);

  // Possibly waits for `element_id` to be painted if it is a WebContents.
  [[nodiscard]] MultiStep MaybeWaitForPaint(ElementSpecifier element);

  Browser* GetBrowserFor(ui::ElementContext current_context,
                         BrowserSpecifier spec);

  internal::InteractiveBrowserTestPrivate& test_impl() {
    return static_cast<internal::InteractiveBrowserTestPrivate&>(
        private_test_impl());
  }
};

// Template for adding InteractiveBrowserTestApi to any test fixture which is
// derived from InProcessBrowserTest.
//
// If you don't need to derive from some existing test class, prefer to use
// InteractiveBrowserTest.
//
// Note that this test fixture attempts to set the context widget from the
// created `browser()` during `SetUpOnMainThread()`. If your derived test
// fixture does not create a browser during set up, you will need to manually
// `SetContextWidget()` before calling `RunTestSequence()`, or use
// `RunTestTestSequenceInContext()` instead.
//
// See README.md for usage.
template <typename T>
  requires std::derived_from<T, InProcessBrowserTest>
class InteractiveBrowserTestT : public T, public InteractiveBrowserTestApi {
 public:
  template <typename... Args>
  explicit InteractiveBrowserTestT(Args&&... args)
      : T(std::forward<Args>(args)...) {}

  ~InteractiveBrowserTestT() override = default;

 protected:
  void SetUpOnMainThread() override {
    T::SetUpOnMainThread();
    private_test_impl().DoTestSetUp();
    if (Browser* browser = T::browser()) {
      SetContextWidget(
          BrowserView::GetBrowserViewForBrowser(browser)->GetWidget());
    }
  }

  void TearDownOnMainThread() override {
    private_test_impl().DoTestTearDown();
    T::TearDownOnMainThread();
  }
};

// Convenience test fixture for interactive browser tests. This is the preferred
// base class for Kombucha tests unless you specifically need something else.
//
// Note that this test fixture attempts to set the context widget from the
// created `browser()` during `SetUpOnMainThread()`. If your derived test
// fixture does not create a browser during set up, you will need to manually
// `SetContextWidget()` before calling `RunTestSequence()`, or use
// `RunTestTestSequenceInContext()` instead.
//
// See README.md for usage.
using InteractiveBrowserTest = InteractiveBrowserTestT<InProcessBrowserTest>;

// Template definitions:

// static
template <typename T>
ui::InteractionSequence::StepBuilder InteractiveBrowserTestApi::CheckJsResult(
    ui::ElementIdentifier webcontents_id,
    const std::string& function,
    T&& value) {
  return std::move(
      CheckElement(
          webcontents_id,
          [function,
           value = ui::test::internal::MatcherTypeFor<T>(
               std::forward<T>(value))](ui::TrackedElement* el) mutable {
            std::string error_msg;
            base::Value result =
                el->AsA<TrackedElementWebContents>()->owner()->Evaluate(
                    function, &error_msg);
            if (!error_msg.empty()) {
              LOG(ERROR) << "CheckJsResult() failed: " << error_msg;
              return false;
            }

            auto m = internal::MakeValueMatcher(std::move(value));
            return ui::test::internal::MatchAndExplain("CheckJsResult()", m,
                                                       result);
          })
          .SetContext(kDefaultWebContentsContextMode)
          .SetDescription(base::StringPrintf("CheckJsResult(\"\n%s\n\")",
                                             function.c_str())));
}

// static
template <typename T>
ui::InteractionSequence::StepBuilder InteractiveBrowserTestApi::CheckJsResultAt(
    ui::ElementIdentifier webcontents_id,
    const DeepQuery& where,
    const std::string& function,
    T&& value) {
  return std::move(
      CheckElement(
          webcontents_id,
          [where, function,
           value = ui::test::internal::MatcherTypeFor<T>(
               std::forward<T>(value))](ui::TrackedElement* el) mutable {
            const auto full_function = base::StringPrintf(
                R"(
            (el, err) => {
              if (err) {
                throw err;
              }
              return (%s)(el);
            }
          )",
                function.c_str());
            std::string error_msg;
            base::Value result =
                el->AsA<TrackedElementWebContents>()->owner()->EvaluateAt(
                    where, full_function, &error_msg);
            if (!error_msg.empty()) {
              LOG(ERROR) << "CheckJsResult() failed: " << error_msg;
              return false;
            }

            auto m = internal::MakeValueMatcher(std::move(value));
            return ui::test::internal::MatchAndExplain("CheckJsResultAt()", m,
                                                       result);
          })
          .SetContext(kDefaultWebContentsContextMode)
          .SetDescription(base::StringPrintf(
              "CheckJsResultAt( %s, \"\n%s\n\")",
              internal::InteractiveBrowserTestPrivate::DeepQueryToString(where)
                  .c_str(),
              function.c_str())));
}

template <typename M>
InteractiveBrowserTestApi::MultiStep
InteractiveBrowserTestApi::WaitForJsResultCommon(
    ui::ElementIdentifier webcontents_id,
    StateChange::Type type,
    const std::string& function,
    const DeepQuery& where,
    M&& value,
    bool continue_across_navigation,
    ui::CustomElementEventType event) {
  StateChange change;
  change.type = type;
  change.test_function = function;
  change.event = event;
  change.continue_across_navigation = continue_across_navigation;
  change.where = where;

  auto context = private_test_impl().CreateAdditionalContext();
  auto expected = ui::test::internal::MatcherTypeFor<M>(std::forward<M>(value));
  using X = decltype(expected);
  change.check_callback = base::BindRepeating(
      [](const X& expected, AdditionalContext context,
         const base::Value& actual) {
        auto m = internal::MakeConstValueMatcher(expected);
        std::ostringstream oss;
        oss << "Expected ";
        m.DescribeTo(&oss);
        oss << "; last known value: " << actual;
        context.Set(oss.str());
        return m.Matches(actual);
      },
      std::move(expected), context);

  return Steps(
      WaitForStateChange(webcontents_id, change),
      Do([context]() mutable { context.Clear(); })
          // Preserve the context of the previous step so that
          // `InSameContext()` on subsequent steps remains valid.
          .SetContext(ui::InteractionSequence::ContextMode::kFromPreviousStep));
}

template <typename M>
InteractiveBrowserTestApi::MultiStep InteractiveBrowserTestApi::WaitForJsResult(
    ui::ElementIdentifier webcontents_id,
    const std::string& function,
    M&& value,
    bool continue_across_navigation,
    ui::CustomElementEventType event) {
  auto steps = WaitForJsResultCommon(
      webcontents_id, StateChange::Type::kConditionTrue, function, DeepQuery(),
      std::forward<M>(value), continue_across_navigation, event);

  std::ostringstream prefix;
  prefix << "WaitForJsResult(" << webcontents_id << ") with function\n"
         << function << "\n";
  AddDescriptionPrefix(steps, prefix.str());
  return steps;
}

template <typename M>
InteractiveBrowserTestApi::MultiStep
InteractiveBrowserTestApi::WaitForJsResultAt(
    ui::ElementIdentifier webcontents_id,
    const DeepQuery& where,
    const std::string& function,
    M&& value,
    bool element_must_be_present_at_start,
    bool continue_across_navigation,
    ui::CustomElementEventType event) {
  auto steps =
      WaitForJsResultCommon(webcontents_id,
                            element_must_be_present_at_start
                                ? StateChange::Type::kConditionTrue
                                : StateChange::Type::kExistsAndConditionTrue,
                            function, where, std::forward<M>(value),
                            continue_across_navigation, event);

  std::ostringstream prefix;
  prefix << "WaitForJsResultAt(" << webcontents_id << ", " << where
         << ") with function\n"
         << function << "\n";
  AddDescriptionPrefix(steps, prefix.str());
  return steps;
}

#endif  // CHROME_TEST_INTERACTION_INTERACTIVE_BROWSER_TEST_H_