blob: 84b34023bc60a956bbfcc4e2d79f4157a2edff46 [file] [view]
# Chromium Interaction Library
Note: for **Interactive Testing**, see the
[Interactive Test Documentation](/chrome/test/interaction/README.md)
This folder contains primitives for locating named elements in different
application windows as well as following specific sequences of user interactions
with the UI. It is designed to support things like User Education Tutorials and
interactive UI testing, and to work with multiple UI presentation frameworks.
See the [Supported Frameworks](#Supported-Frameworks) section for a list of
currently-supported frameworks.
[TOC]
## Named elements
Named elements are the fundamental building blocks of these systems. Each
supported framework must define how a UI element can be assigned an
[ElementIdentifier](/ui/base/interaction/element_identifier.h) which its
[framework implementation](#Supporting-additional-UI-frameworks) must be able to read.
A UI element with a non-null `ElementIdentifier` assigned to it is known as a
*named element*.
Each `ElementIdentifier` contains a globally unique opaque handle, with its
underlying value based on the address of a block of memory allocated at compile
time.
Two named elements with the same identifier are not necessarily equivalent. In
an application with multiple primary windows (such as a browser with several
windows and a PWA open), each primary window and all of its secondary UI - menus,
dialogs, bubbles, etc. - are represented by a single `ElementContext`. Like
identifiers, contexts are opaque handles that are unique to each primary window.
To get the context associated with a UI element or window, you will need to call
a method specific to your framework; see
[the relevant section](#2_Define-how-works-in-your-framework)
below.
`ElementIdentifier` and `ElementContext` both support the methods one might
expect from a handle or opaque pointer:
* Assignment (`=`)
* Equality (`==`, `!=`)
* Ordering (`<`) - for use in `std::set` and `std::map`
* Boolean value (`!` and ```explicit operator bool```)
* Conversion to and from an integer type (`intptr_t`) - for platforms written in
languages that don't support pointers
The only falsy/null/zero value is the default-constructed value. No guarantees
are made about the ordering produced by the `<` operator; only that there is
one.
### Creating `ElementIdentifier` values for your application
You will want to create named constants representing each identifier you want to
use in your code. These constants are defined using macros found in
[element_identifier.h](/ui/base/interaction/element_identifier.h).
Each declaration creates a compile-time constant that can be copied and used
anywhere in your application - they are valid from the time your application
starts up until it exits.
Furthermore, every `ElementIdentifier` you declare in code will either be
default-constructed (and therefore null), or be assigned a copy of one of these
compile-time constants. You should _never_ construct an `ElementIdentifier` or
assign a value from anything other than another `ElementIdentifier`.
To create a public, unique `ElementIdentifier` value in a global scope:
``` cpp
// This goes in the .h file:
DECLARE_ELEMENT_IDENTIFIER_VALUE(kMyIdentifierName);
// This goes in the .cc file:
DEFINE_ELEMENT_IDENTIFIER_VALUE(kMyIdentifierName);
```
To create a class member that is a unique identifier, use:
``` cpp
// This declares a unique identifier, MyClass::kClassMemberIdentifier. You could
// also make the identifier protected or private if you wanted.
class MyClass {
public:
DECLARE_CLASS_ELEMENT_IDENTIFIER_VALUE(MyClass, kClassMemberIdentifier);
};
// This goes in the .cc file:
DEFINE_CLASS_ELEMENT_IDENTIFIER_VALUE(MyClass, kClassMemberIdentifier);
```
And finally, if you want to create an identifier that's local to a .cc file or
class method, there is a single-line declaration available, as shown in these
examples:
``` cpp
// This declares a module-local identifier. The anonymous namespace is optional
// (though recommended) as kModuleLocalIdentifier will also be marked 'static'
// and the identifier's name will contain the file and line.
namespace {
DEFINE_LOCAL_ELEMENT_IDENTIFIER_VALUE(kModuleLocalIdentifier);
}
// This declares a method-local identifier. Again, the use of the local
// identifier will cause the generated name to include file and line number.
/* static */ ui::ElementIdentifier MyClass::GetIdentifier() {
DEFINE_LOCAL_ELEMENT_IDENTIFIER_VALUE(kMethodLocalIdentifier);
return kMethodLocalIdentifier;
}
```
Please note that since all `ElementIdentifier`s created using these macros are
constexpr/compile-time constants, copies of these values can be used outside of
their originating classes or modules - the value of the `ElementIdentifier` is
valid anywhere in the application.
## UI Elements and element events
[`ElementTracker`](/ui/base/interaction/element_tracker.h) is a global singleton
object that allows you to:
* Retrieve a visible element or elements from a particular context by identifier
* Register for callbacks when elements with a given identifier and context
become visible
* Register for callbacks when elements with a given identifier and context
become hidden or are destroyed
* Register for callbacks when the user interacts with an element with a given
identifier and context (known as _activation_)
`TrackedElement`, a polymorphic class defined in
[`element_tracker.h`](/ui/base/interaction/element_tracker.h), represents a
platform-agnostic UI element with an identifier and context. There must be a 1:1
correspondence between a visible named UI element and an
`TrackedElement`; the `TrackedElement` is what is passed to
callbacks when the UI element is shown, hidden, or activated.
Each framework has its own derived version of `TrackedElement` that may
provide additional information about the element. If you know what platform the
element is from you may use the `AsA()` template method to dynamically downcast
to the platform-specific element type. If you are working in an environment with
multiple presentation frameworks, you can use the `IsA()` method to determine if
the element is of the expected type.
Here is an example that shows some of the functionality of `ElementTracker` and
`TrackedElement`. Note that you must specify the `ElementContext` in
which you are listening:
``` cpp
void ListenForShowEvent(ui::ElementIdentifier id, ui::ElementContext context) {
auto callback =
base::BindRepeating(&MyClass::OnElementShown, base::Unretained(this)));
subscription_ = ui::ElementTracker::GetElementTracker()
->AddElementShownCallback(id, context, callback);
}
void OnElementShown(ui::TrackedElement* element) {
// Technically you don't need the IsA() call here, since AsA() returns null if
// the object is the wrong type.
if (element->IsA<views::TrackedElementViews>()) {
views::View* const view =
element->AsA<views::TrackedElementViews>()->view();
// Do something with the view that was shown here.
}
```
Then, in your production code, assign an element identifier to the element you
want to track:
``` cpp
// Note: matching DECLARE macro must go in header file.
DEFINE_CLASS_ELEMENT_IDENTIFIER_VALUE(MyView, kMyElementIdentifier);
MyView::MyView() {
auto* const child_view = AddChildView(std::make_unique<ChildViewType>());
// This child view will now generate events with the given identifier.
// The context will be derived from the widget the parent view is added to.
child_view->SetProperty(views::kElementIdentifierKey, kMyElementIdentifier);
}
```
## Defining and following user interaction sequences
The `InteractionSequence` class provides a way to describe a sequence of
interactions between the application and the user (real or simulated). Sequences
are useful for e.g. creating user education tutorials guiding the user through
the steps of using a new feature, or for simulating user input during
interaction testing.
Each sequence consists of a series of steps of the following types:
* **Shown** - a UI element is or becomes visible to the user
* **Activated** - the element is visible and user clicks on or otherwise interacts with the UI element
* **Hidden** - a UI element is not visible or stops being visible to the user
These are respective to the corresponding events provided by
`ElementTracker`, but it is an important distinction that these signify
both events *and* the current element state.
Once a step runs, interaction sequence will watch for the event/state of
the next step to occur. For example, the next kShown/kHidden step starts if:
1. the element is visible/not visible at start of step
2. the element becomes visible/not visible during or after step transition
If SetTransitionOnlyOnEvent() is set to true, (1) does not apply.
All of the steps must be followed in order, or the sequence is
_aborted_. Callbacks may be registered at the start and end of each
step, and for when the sequence completes or aborts.
Events not in the sequence (other UI elements appearing, focus changes, mouse
hover, scroll) are ignored unless they result in a required UI element being
dismissed (such as if a dialog or menu is closed).
To create an interaction sequence, use a `InteractionSequence::Builder`. To add
steps to the builder, use an `InteractionSequence::StepBuilder`, or call a
convenience method like `WithInitialElement()`. Here is an example that expects
the user to interact with a feature entry point and then displays a help bubble
on the resulting dialog:
``` cpp
initial_element =
ElementTracker::GetFirstMatchingElement(kFeatureEntryPointID, context());
sequence_ = InteractionSequence::Builder()
.SetCompletedCallback(base::BindOnce(
&MyClass::OnSequenceComplete,
base::Unretained(this)))
.AddStep(InteractionSequence::WithInitialElement(initial_element))
.AddStep(InteractionSequence::StepBuilder()
.SetElementID(initial_element->identifier())
.SetType(StepType::kActivated)
.Build())
.AddStep(InteractionSequence::StepBuilder()
.SetElementID(kFeatureDialogID)
.SetType(StepType::kShown)
.SetStartCallback(&MyClass::ShowHelpBubble, base::Unretained(this))
.Build())
.Build();
sequence_->Start();
```
### Interaction steps
Each `Step` has properties that can be set via its `StepBuilder`:
* **Type** - whether this is a show, activate, or hide step
* **Element ID** - the identifier of the element involved in the step
* **Must be visible at start** - the element must be visible at the start of the
step or else the sequence is aborted
* **Must remain visible** - the element must remain visible until the next step
begins; not compatible with **hidden** steps
* **Start callback** - called as soon as the step is started
* **End callback** - called as soon as the next step is started, or the sequence
aborts or completes
Of these, only **Type** and **Element ID** are required. If you do not specify
whether the element must be visible at start or remain visible, default values
will be assigned according to the type of step. All callbacks are optional.
Instead of using `StepBuilder`, for the initial step you can call
`InteractionSequence::WithInitialElement()`. This creates a default **shown**
step for an element that is already visible; it expects the element to be
visible when `Start()` is called or the sequence will abort. You may pass
optional step start and end callbacks to `WithInitialElement()`; these are
useful for displaying an initial prompt to the user (in the case of a
tutorial).
There is an additional method on `StepBuilder`, `SetContext()`, but it is only
used by helper methods and for testing. You should instead use
`Builder::SetContext()` or `InteractionSequence::WithInitialElement()`. There
is currently no support for cross-context sequences and setting conflicting
contexts in a sequence is an error and will crash if DCHECK is enabled.
### Step callbacks
Each step callback (start or end) has three parameters:
* **Element** - the element involved in the step; null if the element is not
available (i.e. was hidden before the callback could be called)
* **Identifier** - the `ElementIdentifier` associated with the step, which is
always valid even if the element is null
* **Type** - the step type; **shown**, **activated**, or **hidden**
The **element** can be used to retrieve the UI element in your framework by
downcasting via `AsA()` - see
[UI Elements and element events](#UI-Elements-and-element-events) above.
Typically, when using a sequence to run a tutorial, this will be the code that
shows or hides a tutorial dialog or prompt. When using the sequence for
interaction testing, the callback will contain the code to simulate the next
input to the UI.
### Best practices
In general, it will be pretty obvious how to construct your sequence, because
you know the steps you need to perform in the UI to get where you want to go.
However, keep the following in mind:
* Try to start the sequence with a step generated by `WithInitialElement()`,
keyed to a UI element you know will be visible when the sequence starts.
* Do not assume the order in which elements will become visible when a surface
is shown.
* Do not assume that interacting with a button or menu item will bring up a
resulting surface (another menu, a dialog) _before_ the initial button or
menu item disappears.
To elaborate on the third point: it is better to have the following steps in the
case where a menu item brings up a dialog:
1. Menu item shown
2. Menu item activated (does not need to remain visible; default)
3. Dialog element shown (does not need to be visible at start; default)
If you specify that the menu item must stay visible or that the dialog element
must be visible at step start, the sequence could fail depending on the order in
which the presentation framework dismisses the menu and displays the dialog.
However, in the case where you want the user to navigate a series of submenus,
if the platform supports menu-open-via-hover you may not receive the
**activated** signal and a sequence like the following might work better:
1. Menu item shown
2. Submenu item shown (triggers as soon as the submenu is opened, regardless of
how)
3. Submenu item activated
4. ...
### Known limitations
* Cannot nest sequences (might be able to in some cases via callbacks)
* Cannot provide alternate sets of steps in the same sequence
* Cannot skip ahead (e.g. if the user uses a shortcut key to bypass a menu)
* Cannot restart steps (e.g. if the user hovers a submenu containing the next
element, then un-hovers it, then hovers it again)
All of these can be addressed if a relevant, concrete need is found.
## Supporting additional UI frameworks
If you want to use ElementTracker with a framework that isn't supported yet, you
must at minimum do the following:
1. Derive a class from `TrackedElement` representing visual elements in
your framework.
2. Determine how `ElementContext`s are defined in your framework.
3. Implement code to create and register your derived element objects with
`ElementTracker` when UI elements become visible to the user, send events
when they are activated by the user (however you choose to define
"activation"), and to unregister them when the element is no longer visible.
See [`ElementTrackerViews`](/ui/views/interaction/element_tracker_views.h) for
an example implementation.
When you are done, please add the folder containing the implementation code to
the [Supported Frameworks](#Supported-Frameworks) section below.
### 1. Derive a class from `TrackedElement`
When you derive a class from `TrackedElement` to use for your UI
framework, you are obliged to declare specific metadata in order to support
`IsA()` and `AsA()`. To do this, add the following to the class definition:
``` cpp
class TrackedElementMyPlatform {
public:
// This provides the required TrackedElement metadata support.
DECLARE_ELEMENT_TRACKER_METADATA();
}
// In the corresponding .cc file:
DEFINE_ELEMENT_TRACKER_METADATA(TrackedElementMyPlatform)
```
You will also be expected to pass an immutable identifier and context into the
constructor, and if the element object stores a pointer or handle to the
associated UI element in your framework, that reference should not change for
the life of the element (and the element should not outlive the corresponding
framework object).
### 2. Define how `ElementContext` works in your framework
You will need a method to generate an `ElementContext` from a window or UI
element. The context identifies the primary window associated with the UI, such
as a browser or PWA window, the taskbar of an operating system's GUI, a file
browser window, etc. Good candidates are the handle of the primary window or the
address of the framework object that represents it. The value of the handle must
not change over the lifetime of that window.
If you do not already have one, you will probably want a helper method that
finds the primary window given a UI element that might be in secondary UI (such
as a dialog or menu).
In Views, we use the address of the primary window's `Widget` to construct the
`ElementContext` and we provide the `ElementTrackerViews::GetContextForView()`
method to fetch it. We also added the `Widget::GetPrimaryWindowWidget()` helper
method for finding the primary window.
### 3. Managing the lifetime of your elements and sending events
How your platform manages the lifetime of elements is entirely up to you. You
could create an `TrackedElement` whenever a named UI element in your
framework becomes visible to the user, or you could have every UI element
with an associated `ElementIdentifier` hold a permanent `TrackedElement`.
The one requirement is that a single `TrackedElement` must be associated
with any named UI element, and must remain associated with that implementation
as long as the element remains visible.
To register or unregister an element or send activation events, get the
`ElementTrackerFrameworkDelegate` from the `ElementTracker` class and call the
appropriate method:
``` cpp
auto* const delegate = ui::ElementTracker::GetFrameworkDelegate();
// Register a visible element:
delegate->NotifyElementShown(my_element);
// Notify that the element was activated by the user:
delegate->NotifyElementActivated(my_element);
// Unregister the element on hide:
delegate->NotifyElementHidden(my_element);
```
"Activation" requires some special discussion here, as what it means to be
activated is extremely context-specific. For example:
* A button is activated when the user clicks it
* A menu item is activated when the user performs the item's action
* A browser tab is activated when the user selects the tab with the mouse or
keyboard
Activation should be the **default** action that occurs when the user directly
interacts with that UI element. So for example, the _Back_ button in a browser
can be clicked to return to the previous page, or long-pressed/dragged to open a
menu containing a list of previous pages. The single-click is the default
action, and therefore should be the action that results in
`NotifyElementActivated()` being called.
How you proxy events from UI elements to calls to
`ElementTrackerFrameworkDelegate` is entirely up to you. In Views we go through
a mediator object -
[`ElementTrackerViews`](/ui/views/interaction/element_tracker_views.h) - which
first maps the `Button`, `MenuItem`, etc. to an `TrackedElementViews`
before passing that object to the appropriate delegate method.
## Supported Frameworks
The following UI frameworks support `ElementTracker` and `InteractionSequence`.
Please add additional frameworks to this list as they become supported.
* Views:
* [`ElementTrackerViews`](/ui/views/interaction/element_tracker_views.h)
* [`InteractionSequenceViews`](/ui/views/interaction/interaction_sequence_views.h)