forked from chromium/chromium
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathinteraction_sequence.h
599 lines (503 loc) · 25.3 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
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
// 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/component_export.h"
#include "base/functional/callback_forward.h"
#include "base/gtest_prod_util.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 "third_party/abseil-cpp/absl/types/variant.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,
// Represents one or more nested, conditional subsequences. An element may
// be provided for use in `SubsequenceCondition` checks. See
// `SubsequenceMode` for more information on how subsequences work.
//
// Note that while a subsequence step can have an element name or ID, it is
// not required. Furthermore, the element will be located at the start of
// the step and if it is not present, null will be passed to the
// SubsequenceCondition (unless must_be_visible is true, in which case the
// step will fail). This allows subsequences to be conditional on the
// presence of an element.
//
// Known limitations:
// - If the triggering condition for the step following this one occurs
// during execution of one of the subsequences, it may be missed/lost.
// - If there is no element specified, or the element does not exist, then
// the following step will not be able to effectively use
// ContextMode::kFromPreviousStep.
kSubsequence,
kMaxValue = kSubsequence
};
// Describes how the subsequences in a `StepType::kSubsequence` step are
// executed.
enum class SubsequenceMode {
// The first subsequence whose condition is met is executed, and the step
// finishes if the subsequence completes. If no subsequences run, the step
// succeeds.
kAtMostOne,
// The first subsequence whose condition is met is executed, and the step
// finishes if the subsequence completes. If no subsequences run, the step
// fails.
kExactlyOne,
// All subsequences whose conditions are met are executed, and the step
// finishes if any of the subsequences completes successfully. The others
// may fail, and are destroyed immediately as soon as the first succeeds. If
// no sequences run, the step fails.
kAtLeastOne,
// All subsequences whose conditions are met are executed, and the step
// finishes if all of the subsequences complete. If no sequences run, the
// step succeeds.
//
// This is the default behavior.
kAll,
kMaxValue = kAll
};
// 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,
// One or more subsequences were expected to run, but none could due to
// failed preconditions.
kNoSubsequenceRun,
// One or more subsequences needed to succeed, but one or more unexpectedly
// failed. Details will be the failed step from the first subsequence that
// should have completed but did not.
kSubsequenceFailed,
// The sequence was explicitly failed as part of a test.
kFailedForTesting,
// Update this if values are added to the enumeration.
kMaxValue = kFailedForTesting
};
// Specifies how the context for a step is determined.
enum class ContextMode {
// Use the initial context for the sequence.
kInitial,
// Search for the element in any context. Currently can only apply to kShown
// steps.
kAny,
// Inherits the context from the previous step. Cannot be used on the first
// step in a sequence.
kFromPreviousStep
};
// Determines whether a subsequence will run. `seq` is the parent sequence,
// and `el` is the reference element, and may be null if the element is not
// specified or if there is no matching element. This is unlike other steps
// where an element is typically required to be present before the step can
// proceed.
using SubsequenceCondition =
base::OnceCallback<bool(const InteractionSequence* seq,
const TrackedElement* el)>;
// Returns a callback that causes the subsequence to always run.
static SubsequenceCondition AlwaysRun();
// A step context is either an explicit context or a ContextMode.
using StepContext = absl::variant<ElementContext, ContextMode>;
// 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)>;
// Information passed when a sequence fails or is aborted.
struct AbortedData {
AbortedData();
~AbortedData();
AbortedData(const AbortedData& other);
AbortedData& operator=(const AbortedData& other);
// The index of the step where the failure occurred. 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).
int step_index = 0;
// The description of the failed step.
std::string step_description;
// The step type of the failed step.
StepType step_type = StepType::kShown;
// A reference to the element used by the failed step. This is a weak
// reference and may be null if the element was hidden or destroyed.
SafeElementReference element;
// The identifier of the element used by the failed step.
ElementIdentifier element_id;
// The reason the step failed/the sequence was aborted.
AbortedReason aborted_reason = AbortedReason::kSequenceDestroyed;
// If this failure was due to a subsequence failing, the failure information
// for the subsequences will be stored here.
std::vector<absl::optional<AbortedData>> subsequence_failures;
};
// 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,
// or when the sequence fails for some other reason.
//
// The most recent step is described by the `AbortedData` block.
using AbortedCallback = base::OnceCallback<void(const AbortedData&)>;
using CompletedCallback = base::OnceClosure;
struct Configuration;
class StepBuilder;
struct SubsequenceData;
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;
StepContext context = ContextMode::kInitial;
// 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;
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;
// Provides a useful description for debugging that can be read or passed
// to the abort callback on failure.
std::string description;
// These only apply if the type of the step is kSubsequence.
SubsequenceMode subsequence_mode = SubsequenceMode::kAll;
std::vector<SubsequenceData> subsequence_data;
};
// 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:
friend class InteractionSequence;
std::unique_ptr<InteractionSequence> BuildSubsequence(
const Step* owning_step);
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 step; useful for setting up the initial
// element of the sequence if you do not know the context ahead of time, or
// to specify that a step should not use the default context.
StepBuilder& SetContext(StepContext 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());
// Changes the subsequence mode from the default. See `SubsequenceMode` for
// details. Implicitly sets the step type to kSubsequence.
StepBuilder& SetSubsequenceMode(SubsequenceMode subsequence_mode);
// Adds a subsequence to the step. The subsequence will run if `condition`
// returns true. Implicitly changes the step type to kSubsequence.
//
// The subsequence will not actually be built until it is needed. It will
// inherit the named elements of its parent unless otherwise specified.
StepBuilder& AddSubsequence(Builder subsequence,
SubsequenceCondition condition = AlwaysRun());
// 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);
// 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);
// Sets the description of the step.
StepBuilder& SetDescription(const base::StringPiece& description);
// Formats the existing description into a new string; allows for adding
// modifiers to an existing description. `format_string` should contain
// exactly one "%s".
StepBuilder& FormatDescription(const base::StringPiece& format_string);
// 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:
FRIEND_TEST_ALL_PREFIXES(InteractionSequenceSubsequenceTest, NamedElements);
explicit InteractionSequence(std::unique_ptr<Configuration> configuration,
const Step* reference_step);
// 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 or staging a subsequence, 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 MaybeWatchForEarlyTrigger(const Step* current_step);
// 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;
// Returns the next step, or null if none.
Step* next_step();
// Returns the context for the current sequence.
ElementContext context() const;
// Updates the next step context from the current based on its StepContext.
// Returns an element context if one is determined; null context if the step
// allows any context.
// Do not call for named elements.
ElementContext UpdateNextStepContext(const Step* current_step);
// Callbacks for when subsequences terminate.
using SubsequenceHandle = const SubsequenceData*;
void OnSubsequenceCompleted(SubsequenceHandle subsequence);
void OnSubsequenceAborted(SubsequenceHandle subsequence,
const AbortedData& aborted_data);
void BuildSubsequences(const Step* current_step);
SubsequenceData* FindSubsequenceData(SubsequenceHandle subsequence);
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 void PrintTo(InteractionSequence::SubsequenceMode mode,
std::ostream* os);
COMPONENT_EXPORT(UI_BASE)
extern void PrintTo(const InteractionSequence::AbortedData& aborted_data,
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);
COMPONENT_EXPORT(UI_BASE)
extern std::ostream& operator<<(std::ostream& os,
InteractionSequence::SubsequenceMode mode);
COMPONENT_EXPORT(UI_BASE)
extern std::ostream& operator<<(
std::ostream& os,
const InteractionSequence::AbortedData& aborted_data);
} // namespace ui
#endif // UI_BASE_INTERACTION_INTERACTION_SEQUENCE_H_