Kombucha is a group of powerful test mix-ins that let you easily and concisely write interactive tests.
The current API version is 2.0. All future 2.x versions are guaranteed to either be backwards-compatible with existing tests, or the authors will update the API calls for you.
This page provides technical documentation. For a cookbook/FAQ/troubleshooting guide, see our Kombucha Playbook.
There are two ways to write a Kombucha-based interaction test:
If you go the latter route, please see Custom Test Fixtures below.
Note: Throughout this section, unless otherwise specified, all methods are present in InteractiveTestApi
. If a method is introduced in InteractiveViewsTestApi
, it will have [Views] next to it; if it's introduced in InteractiveBrowserTestApi
, it will have [Browser] next to it instead.
The primary entry point for any test is RunTestSequence()
[Views] or RunTestSequenceInContext()
. (For more information on ElementContext
, see the Interaction Library Documentation.)
RunTestSequence()
is designed to accept any number of steps. You will use the provided palette of test verbs and checks that the API provides, or create your own verbs through generator methods. The steps are executed in order until either the final step completes, which is considered success, or a step fails (or the test times out), which are considered failures.
Example:
class MyDialogTest : public InteractiveBrowserTest { ... }; IN_PROC_BROWSER_TEST_F(MyDialogTest, Apply) { RunTestSequence( // Open my dialog. PressButton(kShowMyDialogButtonId), WaitForShow(kMyDialogId), // Validate the caption of the Apply button. CheckViewProperty(kApplyButtonId, &LabelButton::GetText, u"Apply"), // Press the button and verify that the state is applied. PressButton(kApplyButtonId), // This is a custom verb created for this test suite. VerifyChangesApplied()); }
Kombucha test verbs are methods that you can call in your test which generate test steps. Most verbs take an ElementSpecifier
- either an ID or a name - describing which UI element the verb should be applied to, but not all do. Some verbs, like Check()
and Do()
don't care about specific elements.
Verbs fall into a number of different categories:
INFO
. See Logging below.Matcher
s, some use callbacks, etc. Examples include:Check()
CheckResult()
CheckElement()
CheckVariable()
CheckView()
[Views]CheckViewProperty()
[Views]Screenshot
[Browser] - compares the target against Skia Gold in pixel tests. See Handling Incompatibilities for how to handle this in non-pixel tests.WaitForShow()
WaitForHide()
WaitForActivated()
WaitForEvent()
WaitForViewProperty()
[Views]WaitForViewPropertyCallback()
[Views]InteractionSequence::StepStartCallback
or it can omit any number of leading arguments; try to be as concise as possible. Examples:AfterShow()
AfterHide()
AfterActivated()
AfterEvent()
WithElement()
WithView()
[Views]EnsurePresent()
EnsureNotPresent()
ActivateSurface()
, SendAccelerator()
) may flake in environments where the test fixture is not running as the only process, so prefer to use those in interactive_ui_tests. Examples:PressButton()
SelectMenuItem()
SelectTab()
SelectDropdownItem()
EnterText()
SendAccelerator()
Confirm()
DoDefaultAction()
ActivateSurface()
ScrollIntoView()
[Views, Browser]MoveMouseTo()
[Views]DragMouseTo()
[Views]ClickMouse()
[Views]ReleaseMouseButton()
[Views]NameElement()
NameElementRelative()
NameView()
[Views]NameChildView()
[Views]NameChildViewByType()
[Views]NameDescendantView()
[Views]NameDescendantViewByType()
[Views]NameViewRelative()
[Views]InstrumentTab()
[Browser]InstrumentNextTab()
[Browser]AddInstrumentedTab()
[Browser]InstrumentNonTabWebView()
[Browser]NavigateWebContents()
[Browser]WaitForWebContentsReady()
[Browser]WaitForWebContentsNavigation()
[Browser]FocusWebContents()
[Browser]WaitForStateChange()
[Browser]*At()
methods take a DeepQuery and operate on a specific DOM element (possibly in a Shadow DOM), while the non-at methods operate at global scope. If you are not sure if the target element exists or the condition is true yet, use WaitForStateChange()
instead. Examples:ExecuteJs()
[Browser]ExecuteJsAt()
[Browser]CheckJsResult()
[Browser]CheckJsResultAt()
[Browser]ObserveState()
PollState()
PollElement()
PollView()
[Views]PollViewProperty()
[Views]WaitForState()
PollState()
PollElement()
PollView()
[Views]StopObservingState()
FlushEvents()
ensures that the next step happens on a fresh message loop rather than being able to chain successive steps.SetOnIncompatibleAction()
changes what the sequence will do when faced with an action that cannot be executed on the current build, environment, or platform. See Handling Incompatibilities for more information and best practices.Example with mouse input:
// Navigate a page, click Back, and verify that the page navigates back // correctly. RunTestSequence( NavigateWebContents(kActiveTabId, kTargetUrl), MoveMouseTo(kBackButtonId), ClickMouse(), WaitForWebContentsNavigation(kPreviousUrl));
Example with a named element:
RunTestSequence( // Identify the first child view of the button container. NameChildView(kDialogButtons, kFirstButton, 0), // Verify that this is the OK button. CheckViewProperty(kFirstButton, &LabelButton::GetText, l10n_util::GetStringUTF16(IDS_OK)), // Press the button. PressButton(kFirstButton));
Many verbs and modifiers, such as Do
, After...
, With...
, Check...
, and If...
take a test function or callback as an argument.
Kombucha allows you to specify these functions in whatever way is clearest and most concise. You may use any of the following:
base::Bind...()
)The following are, therefore, all valid:
void Func() { // ... } IN_PROC_BROWSER_TEST_F(MyTest, TestDo) { auto lambda = [](){ Func(); }; auto once_callback = base::BindOnce(&Func); auto repeating_callback = base::BindRepeating(&Func); int x = 1; int y = 2; RunTestSequence( Do(&Func), Do(lambda), Do(std::move(once_callback)), Do(repeating_callback), Do([x, &y](){ Func(), LOG(INFO) << "Bound args " << x << ", " << y; })); }
Note that a few cases do still require you to use base::Bind...
; specifically, the arguments to actions like NameChildView
and NameDescendantView
. When a verb does require an explicit argument it will be provided in the verb's method signature.
Using the Log
verb allows for printing of any number of arguments. They are sent to log level INFO
when the Log
step is executed. Log
“knows” how to print anything that our logging macros can. So if you can do LOG(INFO) << value
you can print it with the Log
verb.
There are a few different ways to pass values to Log
:
std::ref
, the value that is printed is the value of the variable at the time the Log
step is executed.Log
step runs and the result is printed.Example:
int x = 1; RunTestSequence( // Change the value of x. Do([&](){ ++x; }), // Print out old, current, and computed values. Log("Original value: ", x, " current value: ", std::ref(x), " square of current value: ", [&x](){ return x*x; }));
A modifier wraps around a step or steps and change their behavior.
ElementContext
. Unlike the other modifiers, there are a number of limitations on its use:Ensure
verbs.InSameContext()
or InContext()
instead.RunTestSequence( // This button might be in a different window! InAnyContext(PressButton(kMyButton)), InAnyContext(CheckView(kMyButton, ensure_pressed)));
RunTestSequence( InAnyContext(WaitForShow(kMyButton)), InSameContext(PressButton(kMyButton)));
Browser* const incognito = CreateIncognitoBrowser(); RunTestSequence( /* Do stuff in primary browser context here */ /* ... */ InContext(incognito->window()->GetElementContext(), Steps( PressButton(kAppMenuButton), WaitForShow(kDownloadsMenuItemElementId))));
Kombucha now provides two options for control flow:
In some cases, you may want to execute part of a test only if, for example, a particular flag is set. In order to do this, we provide the various If()
control-flow statements:
If(condition, then_steps[, else_steps])
- executes then_steps
, which can be a single step or a MultiStep
, if condition
returns true. If else_steps
is present, it will be executed if condition
returns false.IfMatches(function, matcher, then_steps[, else_steps])
- same as above but then_steps
executes if the result of function
matches matcher
.IfElement()
, IfElementMatches()
- same as above, but the condition
or function
receives a const pointer to the specified element as an argument. If the element is not visible, the condition receives nullptr
(it does not fail).IfView()
, IfViewMatches()
- same as above, except that the condition takes a const pointer to a View
or View
subclass; if the element is not present, null is passed, but if it is the wrong type, the test fails.IfViewPropertyMatches()
- same as above, but you specify a readonly method on the View rather than an arbitrary function. Syntax is similar to CheckViewProperty()
.Example:
RunTestSequence( /* ... */ // If MyFeature is enabled, it may interfere with the rest of this test, so // toggle its UI off: If(base::Bind(&base::FeatureList::IsEnabled, kMyFeature)), Steps(PressButton(kFeatureToggleButtonElementId), WaitForHide(kMyFeatureUiElementId)), /* Proceed with test... */ )
Note that in the case of elements, if the element isn't present/visible, the step does not fail; condition
will simply receive a null value.
RunTestSequence( /* ... */ // If the side panel is still visible, close it. IfView(kSidePanelElementId, // If the side panel is visible... [](const SidePanel* side_panel) { return side_panel != nullptr; }, // Then press the side panel button to close the side panel. Steps(PressButton(kToolbarSidePanelButtonElementId), WaitForHide(kSidePanelElementId)), // Else note that it was not open. Log("Side panel was already closed.")), /* ... */ )
Matchers are straightforward; consider the following case where we want to open a new tab, but only if there are fewer than two tabs open:
RunTestSequence( /* ... */ IfMatches( // If there are fewer than two tabs... [this]() { return browser()->tab_strip_model()->count(); }, testing::Lt(2), // Then open a new tab: PressButton(kNewTabButtonElementId)), /* ... */ )
Another common case you might want to handle in a test is when multiple events are going to happen, but you can't guarantee the exact order. Because Kombucha tests are sequential, if a test needs to respond to two discrete events with non-deterministic timing, you need to be able to execute multiple steps in parallel.
For this, we provide InParallel()
and AnyOf()
:
InParallel(step[s], step[s], ...)
- Executes each of step[s]
in parallel with each other. All must complete before the main test sequence can proceed.AnyOf(step[s], step[s], ...)
- Executes each of step[s]
in parallel with each other. Only one must complete, at which point the main test sequence proceeds and the other sequences are scuttled.Example:
RunTestSequence( /* ... */ // This button press will cause two asynchronous processes to spawn. PressButton(kStartBackgroundProcessesButtonElementId), InParallel( WaitForEvent(kMyFeatureUiElementID, kUserDataUpdatedEvent), WaitForEvent(kMyFeatureUiElementId, kUiUpdated)), // It's now safe to proceed. /* ... */ )
Avoid executing steps with side-effects during an InParallel()
or AnyOf()
, especially if those steps could affect other subsequences running in parallel.
Avoid relying on any side-effects of a step in an If()
or AnyOf()
in the remainder of the test, as there is no guarantee those steps will be executed (or in the case of AnyOf()
, they may be executed non-deterministically, which is worse). For example:
RunTestSequence( /* ... */ AnyOf( // WARNING: One or both of these buttons will be pressed, but which is not // deterministic! Steps(WaitForShow(kMyElementId1), PressButton(kMyButtonId1)), Steps(WaitForShow(kMyElementId2), PressButton(kMyButtonId2))) )
Triggering conditions for the first step of a conditional or parallel subsequence can occur during the previous step. However, the triggering condition for the first step of the main test sequence following the control structure cannot occur during subsequence execution (it will be lost). For example:
RunTestSequence( /* ... */ PressButton(kButtonElementId), InParallel( // This is okay, since the first step of a subsequence can trigger during // the previous step. WaitForActivate(kButtonElementId), Steps(WaitForEvent(kButtonElementId, kBackgroundProcessEvent), PressButton(kOtherButtonElementId))), // WARNING: This is unsafe as the PressButton() above occurs in a subsequence, // but this action is in the main sequence. WaitForActivate(kOtherButtonElementId) )
Named elements are inherited by conditional or parallel subsequences, but any names that are assigned by the subsequence are not guaranteed to be brought back to the top level test sequence. We may change this behavior in the future.
Sometimes a test won't run on a specific build bot or in a specific environment due to a known incompatibility (as opposed to something legitimately failing). See Known Incompatibilities for more info.
Normally, if you know that the test won't run on an entire platform (i.e. you can use BUILDFLAG()
to differentiate) you should disable or skip the tests in the usual way. But if the distinction is finer-grained (as with the above verbs) The SetOnIncompatibleAction()
verb and OnIncompatibleAction
enumeration are provided.
OnIncompatibleAction::kFailTest
is the default option; if a step fails because of a known incompatibility, the test will fail, and an error message will be printed.OnIncompatibleAction::kSkipTest
immediately skips the test as soon as an incompatibility is detected. Use this option when you know the rest of the test will fail and the test results are invalid. A warning will be printed.OnIncompatibleAction::kHaltTest
immediately halts the sequence but does not fail or skip the test. Use this option when all of the steps leading up to the incompatible one are valid and you want to preserve any non-fatal errors that may have occurred. A warning will still be printed.OnIncompatibleAction::kIgnoreAndContinue
skips the problematic step, prints a warning, and continues the test as if nothing happened. Use this option when the step is incidental to the test, such as taking a screenshot in the middle of a sequence.Do not use SetOnIncompatibleAction()
unless:
BUILDFLAG()
check.Note that you must specify a non-empty reason
when calling SetOnIncompatibleAction()
with any argument except kFailTest
. This string will be printed out as part of the warning that is produced if the step fails.
A feature of InteractiveBrowserTestApi
that it borrows from WebContentsInteractoinTestUtil is the ability to instrument a WebContents
. This does the following:
WebContents
a unique ElementIdentifier
.NavigateWebContents()
and WaitForWebContentsReady()
.WebContents
via WaitForStateChange()
.You may call Instrument verbs during a test sequence.
InstrumentTab()
instruments an existing tab.InstrumentNextTab()
instruments the next tab to be added to or opened in the specified browser.AddInstrumentedTab()
adds a new tab to a browser and instruments it.InstrumentNonTabWebContents()
instruments a piece of primary or secondary UI that uses a WebView
and is not a tab (e.g. the tablet tabstrip or Tab Search dialog).Certain verbs that operate on instrumented WebContents take a DeepQuery
, which provides a path to a DOM element in the WebContents. A DeepQuery
is a sequence of one or more element selectors, as taken by the JavaScript querySelector()
method. A DeepQuery
works as follows:
let cur = document; for (let selector of deepQuery) { if (cur.shadowRoot) cur = cur.shadowRoot; cur = cur.querySelector(selector); }
If at any point the selector fails, the target DOM element is determined not to exist. Often, this fails the test, but might not in all cases.
There is a strong preference to keep DeepQueries as simple as possible, both in number of queries and in complexity of each query, in order to avoid tests being fragile to small changes in page structure.
The following convenience methods are provided to convert a TrackedElement*
to a more specific object, primarily used in functions supplied to WithElement()
or one of the After verbs:
AsView<T>()
- converts the element to a view of the specific type; fails if it is notAsInstrumentedWebContents()
- converts the element to an instrumented WebContents
; fails if it is notExample:
WithElement(kComboBoxId, [](ui::TrackedElement* el) { // Could have also used SelectDropdownItem for this: AsView<ComboBox>(el)->SelectItem(1); }),
There are a number of ways to wait for some asynchronous browser event or state:
WaitForStateChange()
WaitForEvent()
or AfterEvent()
.StateObserver
-derived class, and use ObserveState()
and WaitForState()
to check for your state change.ObserveState()
is powerful but kind of tricky, as you have to declare a helper class that actually tracks the state.
For state that can be observed using an observer pattern (i.e. you could use base::ScopedObservation
), derive from ObservationStateObserver
, which will handle subscribing and unsubscribing; you need only override 1-3 methods.
Otherwise you will need to derive directly from StateObserver
and manage the process yourself.
Here is an example that waits for a property to achieve a specific value using an observer pattern:
class FooStateObserver : public ObservationStateObserver<int, Foo, FooObserver> { public: FooStateObserver(Foo* foo) : ObservationStateObserver<int>(foo) {} ~FooStateObserver() override = default; // ObservationStateObserver: int GetStateObserverInitialState() const override { return source()->value(); } // FooObserver: void OnFooValueChanged(Foo*, int value) override { OnStateObserverStateChanged(value); } void OnFooDestroyed(Foo*) override { OnObservationStateObserverSourceDestroyed(); } }
Here is an example that derives directly from StateObserver
:
class SubscriptionObserver : public StateObserver { public: SubscriptionObserver(SubscribableObject* object) : subscription_( object->AddValueChangedCallback( base::BindRepeating(&SubscriptionObserver::OnValueChanged, base::Unretained(this)))), object_(sub_obj) {} // ObservationStateObserver: int GetStateObserverInitialState() const override { return object_->value(); } private: void OnValueChanged() { OnStateObserverStateChanged(object_->value()); } base::CallbackListSubscription subscription_; raw_ptr<SubscribableObject> object_; };
The next step is to declare your state identifier and call ObserveState()
. StateIdentifier
s are like ElementIdentifier
s except that they also encode the type of the observer, which allows you to be a little more lax when passing in values to the corresponding verbs:
DEFINE_LOCAL_STATE_IDENTIFIER_VALUE(FooStateObserver, kFooState);
The ObserveState()
verb has two versions:
unique_ptr
.std::reference_wrapper
will be unwrapped to get its value.Log()
verb works.Examples:
// These have parameters evaluated when the sequence is built: ObserveState(kFooState, std::make_unique<FooStateObserver>(&foo)) ObserveState(kFooState, foo.get()) // These have a parameter evaluated at runtime: ObserveState(kFooState, std::ref(foo_ptr)) ObserveState(kFooState, base::BindOnce(&FooTest::GetCurrentFoo, base::Unretained(this)))
Waiting for the state to change is as simple as calling WaitForState()
; again you may pass a callback or reference to have the target evaluated at runtime, or a matcher to look for a range of values:
WaitForState(kFooState, 3), WaitForState(kFooState, std::ref(expected_foo_value)), WaitForState(kFooState, &GetExpectedFooValue), WaitForState(kFooState, testing::Ne(3)),
The PollState()
, PollElement()
, and PollView()
verbs can be used when you want to observe a state but there's no established callback or observer pattern established for that state.
For example, if a system only has a MySystem::GetCurrentState()
property but has neither MySystem::AddObserver(MySystemObserver)
or MySystem::AddStateChangeCallback(MySystem::StateChangeCallback)
, you can use PollState()
to monitor the state:
DEFINE_LOCAL_STATE_IDENTIFIER_VALUE( ui::test::PollingStateObserver<MySystem::State>, kMySystemState); RunTestSequence( // Do setup that would cause your system to initialize. PollState(kMySystemState, [](){ return MySystem::GetInstance()->GetCurrentState(); }), WaitForState(kMySystemState, MySystem::State::kReady) // System will be ready now, continue with your test. );
For PollElement()
and PollView()
, the state value is an absl::optional
and if the element or view is not present in the target context the value will be absl::nullopt
.
Be aware that for transient or short-lived states, the correct value might be missed between polls, so polling should only be used for states that should eventually “settle” on the expected value.
By default, a state observer will persist until the end of the test body, and lasts across multiple calls to RunTestSequence()
.
You should ideally write your state observers (polling or otherwise) to handle freeing of resources or underlying objects, e.g. by unregistering an observer on destruction, or by using base::CallbackSubscription
which is safe with respect to destruction of the subscribed object. Polling an element or view is also safe, with the caveat that you might get a different element each time.
However, in some cases it is easier to simply remove the observer than to try to harden it against changes in the underlying object. The StopObservingState()
verb allows you to do this.
Sometimes you will have some common step or check (or set of steps and checks) that you want to duplicate across a number of different test cases in your test fixture. You can create a custom verb, which is just a method that returns a StepBuilder
or MultiStep
. This method can combine existing verbs with steps you create yourself, in any combination. To combine multiple steps, use the Steps()
method.
Here's an example of a very common custom verb pattern:
// My test fixture class with a custom verb. class MyHistoryTest : public InteractiveBrowserTest { // This custom verb will be used across multiple test cases. auto OpenHistoryPageInNewTab() { return Steps( InstrumentNextTab(kHistoryPageTabId), PressButton(kNewTabButton), PressButton(kAppMenuButton), SelectMenuItem(kHistoryMenuItem), SelectMenuItem(kOpenHistoryPageMenuItem), WaitForWebContentsNavigation(kHistoryPageTabId, chrome::kHistoryPageUrl)); } }; // An example test case. IN_PROC_BROWSER_TEST_F(MyHistoryTest, NavigateTwoPagesAndCheckHistory) { InstrumentTab(browser(), kPrimaryTabId); RunTestSequence( WaitForWebContentsReady(kPrimaryTabId), NavigateWebContents(kPrimaryTabId, kUrl1), NavigateWebContents(kPrimaryTabId, kUrl2), // The custom verb sits happily in the action sequence. OpenHistoryPageInNewTab(), // We'll hand-wave the implementation of this method for now. WaitForStateChange(kHistoryPageTabId, HistoryEntriesPopulated(kUrl1, kUrl2))); }
Another common pattern is having a check that you want to perform over and over; for example, checking that a histogram entry was added. This can absolutely be done through a custom verb, however, perhaps you instead want to use it in an AfterShow()
step. In this case you can create a function that binds and returns the appropriate callback.
class MyDialogTest : public InteractiveBrowserTest { auto ExpectHistogramCount(const char* histogram_name, size_t expected_count) { return base::BindLambdaForTesting([histogram_name, expected_count, this](){ EXPECT_EQ(expected_count, GetHistogramCount(histogram_name)); }); } }; IN_PROC_BROWSER_TEST_F(MyDialogTest, ShowIncrementsHistogram) { RunTestSequence( PressButton(kShowMyDialogButtonId), AfterShow(kMyDialogContentsId, ExpectHistogramCount(kDialogShownHistogramName, 1))); }
This could have also been implemented with a WaitForShow()
and a custom verb with a Check()
or CheckResult()
. Whether you use a custom callback or a custom verb is up to you; do whatever makes your test easiest to read!
Most Kombucha tests will derive directly from either InteractiveViewsTest
or InteractiveBrowserTest
.
If your test needs to derive from a different/custom test fixture class but you would still like access to the Kombucha API, use InteractiveViewsTestT<T>
or InteractiveBrowserTestT<T>
instead.
Example:
// Want Kombucha functionality, but already have an existing test // `MyCustomBrowserTest` with logic we need. using MyTestFixture = InteractiveBrowserTestT<MyCustomBrowserTest>; // Here's another way to do the same thing, if we want to further extend the // test class. class MyTestFixture2 : public InteractiveBrowserTestT<MyCustomBrowserTest> { public: MyTestFixture2(); ~MyTestFixture2() override; // Add anything else we need here. };
Kombucha helper classes are older, lower-level APIs that have been repurposed to support interactive testing:
InteractionTestUtil
, InteractionTestUtilView
, InteractionTestUtilBrowser
- provide common UI functionality like pressing buttons, selecting menu items, and taking screenshots.InteractionTestUtilMouse
- provides a way to inject mouse input, including clicking and dragging, into interactive tests.WebContentsInteractionTestUtil
- provides a way to gain control of a WebContents, inject code, trigger and wait for navigation, and check and wait for changes in the DOM.You should only rarely have to use these classes directly; if you do, it's likely that Kombucha is missing some common verb that would cover your use case. Please reach out to us!
Quality of life improvements:
InteractiveBrowserTestApi
.base::BindOnce()
and base::BindLambdaForTesting()
are no longer required in many cases.New control-flow features (see Control Flow for more info):
InParallel
runs several subsequences at once.If
, IfMatches
, IfView
, etc. conditionally run a subsequence.Bugfixes:
The following will generate an error unless explicitly handled:
ActivateSurface()
does not work on the linux-wayland
buildbot unless the surface is already active, due to vanilla Wayland not supporting programmatic window activation.Screenshot()
currently only works in specific pixel test jobs on the win-rel
buildbot.The following may produce unexpected or inconsistent behavior:
ClickMouse()
is used on Mac with right mouse button, events are sent asynchronously to avoid getting caught in a context menu run loop.DragMouse()
or MoveMouseTo()
on Windows may result in Windows entering a drag loop that may hang or otherwise impact the test.To be supported in the near-future: