forked from chromium/chromium
-
Notifications
You must be signed in to change notification settings - Fork 0
/
interaction_sequence.h
440 lines (369 loc) · 18.7 KB
/
interaction_sequence.h
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
// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef UI_BASE_INTERACTION_INTERACTION_SEQUENCE_H_
#define UI_BASE_INTERACTION_INTERACTION_SEQUENCE_H_
#include <map>
#include "base/callback_forward.h"
#include "base/component_export.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/weak_ptr.h"
#include "base/strings/string_piece.h"
#include "base/strings/string_piece_forward.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
#include "ui/base/interaction/element_identifier.h"
#include "ui/base/interaction/element_tracker.h"
namespace ui {
// Follows an expected sequence of user-UI interactions and provides callbacks
// at each step. Useful for creating interaction tests and user tutorials.
//
// An interaction sequence consists of an ordered series of steps, each of which
// refers to an interface element tagged with a ElementIdentifier and each of
// which represents that element being either shown, activated, or hidden. Other
// unrelated events such as element hover or focus are ignored (but could be
// supported in the future).
//
// Each step has an optional callback that is triggered when the expected
// interaction happens, and an optional callback that is triggered when the step
// ends - either because the next step has started or because the user has
// aborted the sequence (typically by dismissing UI such as a dialog or menu,
// resulting in the element from the current step being hidden/destroyed). Once
// the first callback is called/the step starts, the second callback will always
// be called.
//
// Furthermore, when the last step in the sequence completes, in addition to its
// end callback, an optional sequence-completed callback will be called. If the
// user aborts the sequence or if this object is destroyed, then an optional
// sequence-aborted callback is called instead.
//
// To use a InteractionSequence, start with a builder:
//
// sequence_ = InteractionSequence::Builder()
// .SetCompletedCallback(base::BindOnce(...))
// .AddStep(InteractionSequence::WithInitialElement(initial_element))
// .AddStep(InteractionSequence::StepBuilder()
// .SetElementID(kDialogElementID)
// .SetType(StepType::kShown)
// .SetStartCallback(...)
// .Build())
// .AddStep(...)
// .Build();
// sequence_->Start();
//
// For more detailed instructions on using the ui/base/interaction library, see
// README.md in this folder.
//
class COMPONENT_EXPORT(UI_BASE) InteractionSequence {
public:
// The type of event that is expected to happen next in the sequence.
enum class StepType {
// Represents the element with the specified ID becoming visible to the
// user, or already being visible when the step starts.
kShown,
// Represents an element with the specified ID becoming activated by the
// user (for buttons or menu items, being clicked).
kActivated,
// Represents an element with the specified ID becoming hidden or
// destroyed, or no elements with the specified ID being visible.
kHidden,
// Represents a custom event with a specific custom event type. You may
// further specify a required element name or ID to filter down which
// events you actually want to step on vs. ignore.
kCustomEvent,
// Update this if values are added to the enumeration.
kMaxValue = kCustomEvent
};
// Details why a sequence was aborted.
enum class AbortedReason {
// External code destructed this object before the sequence could complete.
kSequenceDestroyed,
// The starting element was hidden before the sequence started.
kElementHiddenBeforeSequenceStart,
// An element should have been visible at the start of a step but was not.
kElementNotVisibleAtStartOfStep,
// An element should have remained visible during a step but did not.
kElementHiddenDuringStep,
// The sequence was explicitly failed as part of a test.
kFailedForTesting,
// Update this if values are added to the enumeration.
kMaxValue = kFailedForTesting
};
// Callback when a step in the sequence starts. If |element| is no longer
// available, it will be null.
using StepStartCallback =
base::OnceCallback<void(InteractionSequence* sequence,
TrackedElement* element)>;
// Callback when a step in the sequence ends. If |element| is no longer
// available, it will be null.
using StepEndCallback = base::OnceCallback<void(TrackedElement* element)>;
// Callback for when the user aborts the sequence by failing to follow the
// sequence of steps, or if this object is deleted after the sequence starts.
// The most recent event is described by the parameters; if the target element
// is no longer available it will be null.
//
// The active step will be 0 before the sequence starts, and is incremented on
// each step transition after the previous step's end callback is called, or
// if the next step's precondition fails (so that it refers to the correct
// step).
using AbortedCallback =
base::OnceCallback<void(int active_step,
TrackedElement* last_element,
ElementIdentifier last_id,
StepType last_step_type,
AbortedReason aborted_reason)>;
using CompletedCallback = base::OnceClosure;
struct Configuration;
class StepBuilder;
struct COMPONENT_EXPORT(UI_BASE) Step {
Step();
Step(const Step& other) = delete;
void operator=(const Step& other) = delete;
~Step();
bool uses_named_element() const { return !element_name.empty(); }
StepType type = StepType::kShown;
ElementIdentifier id;
CustomElementEventType custom_event_type;
std::string element_name;
ElementContext context;
// These will always have values when the sequence is built, but can be
// unspecified during construction. If unspecified, they will be set to
// appropriate defaults for `type`.
absl::optional<bool> must_be_visible;
absl::optional<bool> must_remain_visible;
bool transition_only_on_event = false;
bool any_context = false;
StepStartCallback start_callback;
StepEndCallback end_callback;
ElementTracker::Subscription subscription;
// Tracks the element associated with the step, if known. We could use a
// SafeElementReference here, but there are cases where we want to do
// additional processing if this element goes away, so we'll add the
// listeners manually instead.
raw_ptr<TrackedElement, DanglingUntriaged> element = nullptr;
};
// Use a Builder to specify parameters when creating an InteractionSequence.
class COMPONENT_EXPORT(UI_BASE) Builder {
public:
Builder();
Builder(Builder&& other);
Builder& operator=(Builder&& other);
~Builder();
// Sets the callback if the user exits the sequence early.
Builder& SetAbortedCallback(AbortedCallback callback);
// Sets the callback if the user completes the sequence.
// Convenience method so that the last step's end callback doesn't need to
// have special logic in it.
Builder& SetCompletedCallback(CompletedCallback callback);
// Adds an expected step in the sequence. All sequences must have at least
// one step.
Builder& AddStep(std::unique_ptr<Step> step);
// Convenience methods to add a step when using a StepBuilder.
Builder& AddStep(StepBuilder& step_builder);
// Convenience method for cases where we don't have an lvalue.
Builder& AddStep(StepBuilder&& step_builder);
// Sets the context for this sequence. Must be called if no step is added
// by element or has had SetContext() called. Typically the initial step of
// a sequence will use WithInitialElement() so it won't be necessary to call
// this method.
Builder& SetContext(ElementContext context);
// Creates the InteractionSequence. You must call Start() to initiate the
// sequence; sequences cannot be re-used, and a Builder is no longer valid
// after Build() is called.
std::unique_ptr<InteractionSequence> Build();
private:
std::unique_ptr<Configuration> configuration_;
};
// Used inline in calls to Builder::AddStep to specify step parameters.
class COMPONENT_EXPORT(UI_BASE) StepBuilder {
public:
StepBuilder();
~StepBuilder();
StepBuilder(StepBuilder&& other);
StepBuilder& operator=(StepBuilder&& other);
// Sets the unique identifier for this step. Either this or
// SetElementName() is required for all step types except kCustomEvent.
StepBuilder& SetElementID(ElementIdentifier element_id);
// Sets the step to refer to a named element instead of an
// ElementIdentifier. Either this or SetElementID() is required for all
// step types other than kCustomEvent.
StepBuilder& SetElementName(const base::StringPiece& name);
// Sets the context for the element; useful for setting up the initial
// element of the sequence if you do not know the context ahead of time.
// Prefer to use Builder::SetContext() if possible.
StepBuilder& SetContext(ElementContext context);
// Sets the type of step. Required. You must set `event_type` if and only
// if `step_type` is kCustomEvent.
StepBuilder& SetType(
StepType step_type,
CustomElementEventType event_type = CustomElementEventType());
// Indicates that the specified element must be visible at the start of the
// step. Defaults to true for StepType::kActivated, false otherwise. Failure
// To meet this condition will abort the sequence.
StepBuilder& SetMustBeVisibleAtStart(bool must_be_visible);
// Indicates that the specified element must remain visible throughout the
// step once it has been shown. Defaults to true for StepType::kShown, false
// otherwise (and incompatible with StepType::kHidden). Failure to meet this
// condition will abort the sequence.
StepBuilder& SetMustRemainVisible(bool must_remain_visible);
// For kShown and kHidden events, if set to true, only allows a step
// transition to happen when a "shown" or "hidden" event is received, and
// not if an element is already visible (in the case of kShown steps) or no
// elements are visible (in the case of kHidden steps).
//
// Default is false. Has no effect on kActiated events which are discrete
// rather than stateful.
//
// Note: Does not track events fired during previous step's start callback,
// so should not be used in automated interaction testing. The default
// behavior should be fine for these cases.
//
// Note: Be careful when setting this value to true, as it increases the
// likelihood of ending up in a state where a failure cannot be detected;
// that is, waiting for an element to appear and then it... never does. In
// this case, you will need an external way to terminate the sequence (a
// timeout, user interaction, etc.)
StepBuilder& SetTransitionOnlyOnEvent(bool transition_only_on_event);
// Specifies whether the step can refer to an element in any context.
// Not compatible with SetContext() or SetElementName(). Currently only
// supported for kShown events.
StepBuilder& SetFindElementInAnyContext(bool any_context);
// Sets the callback called at the start of the step.
StepBuilder& SetStartCallback(StepStartCallback start_callback);
// Sets the callback called at the start of the step. Convenience method
// that eliminates the InteractionSequence argument if you do not need it.
StepBuilder& SetStartCallback(
base::OnceCallback<void(TrackedElement*)> start_callback);
// Sets the callback called at the start of the step. Convenience method
// that eliminates both arguments if you do not need them.
StepBuilder& SetStartCallback(base::OnceClosure start_callback);
// Sets the callback called at the end of the step. Guaranteed to be called
// if the start callback is called, before the start callback of the next
// step or the sequence aborted or completed callback. Also called if this
// object is destroyed while the step is still in-process.
StepBuilder& SetEndCallback(StepEndCallback end_callback);
// Sets the callback called at the end of the step. Convenience method if
// you don't need the parameter.
StepBuilder& SetEndCallback(base::OnceClosure end_callback);
// Builds the step. The builder will not be valid after calling Build().
std::unique_ptr<Step> Build();
private:
friend class InteractionSequence;
std::unique_ptr<Step> step_;
};
// Returns a step with the following values already set, typically used as the
// first step in a sequence (because the first element is usually present):
// ElementID: element->identifier()
// MustBeVisibleAtStart: true
// MustRemainVisible: true
//
// This is a convenience method and also removes the need to call
// Builder::SetContext(). Specific framework implementations may provide
// wrappers around this method that allow direct conversion from framework UI
// elements (e.g. a views::View) to the target element.
static std::unique_ptr<Step> WithInitialElement(
TrackedElement* element,
StepStartCallback start_callback = StepStartCallback(),
StepEndCallback end_callback = StepEndCallback());
~InteractionSequence();
// Starts the sequence. All of the elements in the sequence must belong to the
// same top-level application window (which includes menus, bubbles, etc.
// associated with that window).
void Start();
// Starts the sequence and does not return until the sequence either
// completes or aborts. Events on the current thread continue to be processed
// while the method is waiting, so this will not e.g. block the browser UI
// thread from handling inputs.
//
// This is a test-only method since production code applications should
// always run asynchronously.
void RunSynchronouslyForTesting();
// Explicitly fails the sequence.
void FailForTesting();
// Assigns an element to a given name. The name is local to this interaction
// sequence. It is valid for `element` to be null; in this case, we are
// explicitly saying "there is no element with this name [yet]".
//
// It is safe to call this method from a step start callback, but not a step
// end or aborted callback, as in the latter case the sequence might be in
// the process of being destructed.
void NameElement(TrackedElement* element, const base::StringPiece& name);
// Retrieves a named element, which may be null if we specified "no element"
// or if the element has gone away.
//
// It is safe to call this method from a step start callback, but not a step
// end or aborted callback, as in the latter case the sequence might be in
// the process of being destructed.
TrackedElement* GetNamedElement(const base::StringPiece& name);
const TrackedElement* GetNamedElement(const base::StringPiece& name) const;
private:
explicit InteractionSequence(std::unique_ptr<Configuration> configuration);
// Callbacks from the ElementTracker.
void OnElementShown(TrackedElement* element);
void OnElementActivated(TrackedElement* element);
void OnElementHidden(TrackedElement* element);
void OnCustomEvent(TrackedElement* element);
// Callbacks used only during step transitions to cache certain events.
void OnTriggerDuringStepTransition(TrackedElement* element);
void OnElementHiddenDuringStepTransition(TrackedElement* element);
void OnElementHiddenWaitingForActivate(TrackedElement* element);
// While we're transitioning steps, it's possible for an activation that
// would trigger the following step to come in. This method adds a callback
// that's valid only during the step transition to watch for this event.
void MaybeWatchForTriggerDuringStepTransition();
// A note on the next three methods - DoStepTransition(), StageNextStep(), and
// Abort(): To prevent re-entrancy issues, they must always be the final call
// in any method before it returns. This greatly simplifies the consistency
// checks and safeguards that need to be put into place to make sure we aren't
// making contradictory changes to state or calling callbacks in the wrong
// order.
// Perform the transition from the current step to the next step.
void DoStepTransition(TrackedElement* element);
// Looks at the next step to determine what needs to be done. Called at the
// start of the sequence and after each subsequent step starts.
void StageNextStep();
// Cancels the sequence and cleans up.
void Abort(AbortedReason reason);
// Returns true (and does some sanity checking) if the sequence was aborted
// during the most recent callback.
bool AbortedDuringCallback() const;
// Returns true if `name` is non-empty and `element` matches the element
// with the specified name, or if `name` is empty (indicating we don't care
// about it being a named element). Otherwise returns false.
bool MatchesNameIfSpecified(const TrackedElement* element,
const base::StringPiece& name) const;
// The following would be inline if not for the fact that the data that holds
// the values is an implementation detail.
// Returns the next step, or null if none.
Step* next_step();
// Returns the context for the current sequence.
ElementContext context() const;
// Returns the correct context for the target element if present; defaults to
// context() if `target` is null.
ElementContext GetElementContext(const TrackedElement* target) const;
int active_step_index_ = 0;
bool missing_first_element_ = false;
bool started_ = false;
bool trigger_during_callback_ = false;
bool processing_step_ = false;
std::unique_ptr<Step> current_step_;
ElementTracker::Subscription next_step_hidden_subscription_;
std::unique_ptr<Configuration> configuration_;
std::map<std::string, SafeElementReference> named_elements_;
base::OnceClosure quit_run_loop_closure_for_testing_;
// This is necessary because this object could be deleted during any callback,
// and we don't want to risk a UAF if that happens.
base::WeakPtrFactory<InteractionSequence> weak_factory_{this};
};
COMPONENT_EXPORT(UI_BASE)
extern void PrintTo(InteractionSequence::StepType step_type, std::ostream* os);
COMPONENT_EXPORT(UI_BASE)
extern void PrintTo(InteractionSequence::AbortedReason reason,
std::ostream* os);
COMPONENT_EXPORT(UI_BASE)
extern std::ostream& operator<<(std::ostream& os,
InteractionSequence::StepType step_type);
COMPONENT_EXPORT(UI_BASE)
extern std::ostream& operator<<(std::ostream& os,
InteractionSequence::AbortedReason reason);
} // namespace ui
#endif // UI_BASE_INTERACTION_INTERACTION_SEQUENCE_H_