// 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. #include "ui/base/interaction/interaction_sequence.h" #include "base/bind.h" #include "base/callback_forward.h" #include "base/callback_list.h" #include "base/logging.h" #include "base/memory/ptr_util.h" #include "base/memory/weak_auto_reset.h" #include "base/memory/weak_ptr.h" #include "base/run_loop.h" #include "base/scoped_observation.h" #include "base/strings/string_piece.h" #include "ui/base/interaction/element_identifier.h" #include "ui/base/interaction/element_tracker.h" namespace ui { namespace { // Runs |callback| if it is valid. // We have a lot of callbacks that can be null, so calling through this method // prevents accidentally trying to run a null callback. template void RunIfValid(base::OnceCallback callback, Args... args) { if (callback) std::move(callback).Run(args...); } // Insert an unused argument `Arg` in the front of the argument list for // `callback`, and return the new callback with the dummy argument. template base::OnceCallback PushUnusedArg( base::OnceCallback callback) { return base::BindOnce([](base::OnceCallback callback, Arg arg, Args... args) { std::move(callback).Run(args...); }, std::move(callback)); } // Insert two unused arguments `Arg1` and `Arg2` in the front of the argument // list for `callback`, and return the new callback with the dummy arguments. template base::OnceCallback PushUnusedArgs2( base::OnceCallback callback) { return base::BindOnce( [](base::OnceCallback callback, Arg1 arg1, Arg2 arg2, Args... args) { std::move(callback).Run(args...); }, std::move(callback)); } // Sets step->must_remain_visible if it does not have a value. void SetDefaultMustRemainVisibleValue(InteractionSequence::Step* step, const InteractionSequence::Step* next) { if (step->must_remain_visible.has_value()) return; // Default for types other than kShown is false. if (step->type != InteractionSequence::StepType::kShown) { step->must_remain_visible = false; return; } // If the following step is to hide the same element, the default is false. if (next && next->type == InteractionSequence::StepType::kHidden && (next->id == step->id || next->element_name == step->element_name)) { step->must_remain_visible = false; return; } // If the following step is a re-show of the same element or element ID, the // default is false. if (next && next->type == InteractionSequence::StepType::kShown && next->id == step->id && next->transition_only_on_event) { step->must_remain_visible = false; return; } // Otherwise for kShown steps, the default is true. step->must_remain_visible = true; } } // anonymous namespace InteractionSequence::Step::Step() = default; InteractionSequence::Step::~Step() = default; struct InteractionSequence::Configuration { Configuration() = default; ~Configuration() = default; std::list> steps; ElementContext context; AbortedCallback aborted_callback; CompletedCallback completed_callback; }; InteractionSequence::Builder::Builder() : configuration_(std::make_unique()) {} InteractionSequence::Builder::~Builder() { DCHECK(!configuration_); } InteractionSequence::Builder& InteractionSequence::Builder::SetAbortedCallback( AbortedCallback callback) { DCHECK(!configuration_->aborted_callback); configuration_->aborted_callback = std::move(callback); return *this; } InteractionSequence::Builder& InteractionSequence::Builder::SetCompletedCallback(CompletedCallback callback) { DCHECK(!configuration_->completed_callback); configuration_->completed_callback = std::move(callback); return *this; } InteractionSequence::Builder& InteractionSequence::Builder::AddStep( std::unique_ptr step) { // Do consistency checks and set up defaults. const bool is_custom_event_any_element = step->type == StepType::kCustomEvent && !step->id && !step->uses_named_element(); DCHECK(is_custom_event_any_element || !step->id == step->uses_named_element()) << " A step must set an identifier or a name, but not both."; DCHECK(configuration_->steps.empty() || !step->element) << " Only the initial step of a sequence may have a pre-set element."; DCHECK(!step->transition_only_on_event || !step->element) << " Pre-set element precludes transition_only_on_event."; DCHECK(!step->any_context || (!step->uses_named_element() && !step->context)) << " Find in any context requires no context or name to be set."; DCHECK(!step->any_context || step->type == StepType::kShown) << " Currently, find in any context only supports StepType::kShown."; // Set reasonable defaults for must_be_visible based on step type and // parameters. if (step->uses_named_element() && step->type != StepType::kHidden) { DCHECK(!step->must_be_visible.has_value() || step->must_be_visible.value()) << "Named elements not being hidden must be visible at step start."; step->must_be_visible = true; } else if (is_custom_event_any_element) { DCHECK(!step->must_be_visible.has_value() || !step->must_be_visible.value()) << "A custom event with no element restrictions cannot specify that" " its element must start visible, as we will not know which element" " to refer to."; step->must_be_visible = false; } else { step->must_be_visible = step->must_be_visible.value_or(step->type == StepType::kActivated || step->type == StepType::kCustomEvent); } DCHECK(!step->element || step->must_be_visible.value()) << " Initial step with associated element must be visible from start."; DCHECK(step->type != InteractionSequence::StepType::kHidden || !step->must_remain_visible.has_value() || !step->must_remain_visible.value()) << "Hide steps cannot specify that the element should remain visible."; DCHECK(step->type != InteractionSequence::StepType::kShown || !step->uses_named_element() || !step->transition_only_on_event) << " kShown steps with transition_only_on_event are not compatible with" " named elements since a named element ceases to be valid when it" " becomes hidden."; if (!configuration_->context) { configuration_->context = step->context; } else { DCHECK(!step->context || step->context == configuration_->context) << "Cannot [currently] change context during a sequence."; } // Since the must_remain_visible value can be dependent on the following // step, we'll set it on the previous step, then set it on the final step // when we build the sequence. if (!configuration_->steps.empty()) { SetDefaultMustRemainVisibleValue(configuration_->steps.back().get(), step.get()); } // Add the step. configuration_->steps.emplace_back(std::move(step)); return *this; } InteractionSequence::Builder& InteractionSequence::Builder::AddStep( InteractionSequence::StepBuilder& step_builder) { return AddStep(step_builder.Build()); } InteractionSequence::Builder& InteractionSequence::Builder::SetContext( ElementContext context) { configuration_->context = context; return *this; } std::unique_ptr InteractionSequence::Builder::Build() { DCHECK(!configuration_->steps.empty()); // Configure defaults for the final step. SetDefaultMustRemainVisibleValue(configuration_->steps.back().get(), nullptr); DCHECK(configuration_->context) << "If no view is provided, Builder::SetContext() must be called."; return base::WrapUnique(new InteractionSequence(std::move(configuration_))); } InteractionSequence::StepBuilder::StepBuilder() : step_(std::make_unique()) {} InteractionSequence::StepBuilder::~StepBuilder() = default; InteractionSequence::StepBuilder& InteractionSequence::StepBuilder::SetElementID(ElementIdentifier element_id) { DCHECK(element_id); step_->id = element_id; return *this; } InteractionSequence::StepBuilder& InteractionSequence::StepBuilder::SetElementName( const base::StringPiece& name) { step_->element_name = std::string(name); return *this; } InteractionSequence::StepBuilder& InteractionSequence::StepBuilder::SetContext( ElementContext context) { DCHECK(context); step_->context = context; return *this; } InteractionSequence::StepBuilder& InteractionSequence::StepBuilder::SetMustBeVisibleAtStart( bool must_be_visible) { step_->must_be_visible = must_be_visible; return *this; } InteractionSequence::StepBuilder& InteractionSequence::StepBuilder::SetMustRemainVisible( bool must_remain_visible) { step_->must_remain_visible = must_remain_visible; return *this; } InteractionSequence::StepBuilder& InteractionSequence::StepBuilder::SetTransitionOnlyOnEvent( bool transition_only_on_event) { step_->transition_only_on_event = transition_only_on_event; return *this; } InteractionSequence::StepBuilder& InteractionSequence::StepBuilder::SetFindElementInAnyContext(bool any_context) { step_->any_context = any_context; return *this; } InteractionSequence::StepBuilder& InteractionSequence::StepBuilder::SetType( StepType step_type, CustomElementEventType event_type) { DCHECK_EQ(step_type == StepType::kCustomEvent, static_cast(event_type)) << "Custom events require an event type; event type may not be specified" " for other step types."; step_->type = step_type; step_->custom_event_type = event_type; return *this; } InteractionSequence::StepBuilder& InteractionSequence::StepBuilder::SetStartCallback( StepStartCallback start_callback) { step_->start_callback = std::move(start_callback); return *this; } InteractionSequence::StepBuilder& InteractionSequence::StepBuilder::SetStartCallback( base::OnceCallback start_callback) { step_->start_callback = PushUnusedArg(std::move(start_callback)); return *this; } InteractionSequence::StepBuilder& InteractionSequence::StepBuilder::SetStartCallback( base::OnceClosure start_callback) { step_->start_callback = PushUnusedArgs2( std::move(start_callback)); return *this; } InteractionSequence::StepBuilder& InteractionSequence::StepBuilder::SetEndCallback(StepEndCallback end_callback) { step_->end_callback = std::move(end_callback); return *this; } InteractionSequence::StepBuilder& InteractionSequence::StepBuilder::SetEndCallback( base::OnceClosure end_callback) { step_->end_callback = PushUnusedArg(std::move(end_callback)); return *this; } std::unique_ptr InteractionSequence::StepBuilder::Build() { return std::move(step_); } InteractionSequence::InteractionSequence( std::unique_ptr configuration) : configuration_(std::move(configuration)) { TrackedElement* const first_element = next_step()->element; if (first_element) { DCHECK(first_element->identifier() == next_step()->id); DCHECK(first_element->context() == context()); next_step()->subscription = ElementTracker::GetElementTracker()->AddElementHiddenCallback( first_element->identifier(), first_element->context(), base::BindRepeating(&InteractionSequence::OnElementHidden, base::Unretained(this))); } } // static std::unique_ptr InteractionSequence::WithInitialElement(TrackedElement* element, StepStartCallback start_callback, StepEndCallback end_callback) { StepBuilder step; step.step_->element = element; step.SetType(StepType::kShown) .SetElementID(element->identifier()) .SetContext(element->context()) .SetMustBeVisibleAtStart(true) .SetMustRemainVisible(true) .SetStartCallback(std::move(start_callback)) .SetEndCallback(std::move(end_callback)); return step.Build(); } InteractionSequence::~InteractionSequence() { // We can abort during a step callback, but we cannot destroy this object. if (started_) Abort(AbortedReason::kSequenceDestroyed); } void InteractionSequence::Start() { // Ensure we're not already started. DCHECK(!started_); started_ = true; if (missing_first_element_) { Abort(AbortedReason::kElementHiddenBeforeSequenceStart); return; } StageNextStep(); } void InteractionSequence::RunSynchronouslyForTesting() { base::RunLoop run_loop; quit_run_loop_closure_for_testing_ = run_loop.QuitClosure(); Start(); run_loop.Run(); } void InteractionSequence::NameElement(TrackedElement* element, const base::StringPiece& name) { DCHECK(!name.empty()); named_elements_[std::string(name)] = SafeElementReference(element); DCHECK(!current_step_ || current_step_->element_name != name); // When possible, preload ids for named elements so we can report a more // correct identifier on abort. for (const auto& step : configuration_->steps) { if (step->element_name == name) step->id = element ? element->identifier() : ElementIdentifier(); } // If this is called during a step transition, we may want to watch for // activation or event on the element for the next step. (If the next step // doesn't refer to the element we just named, it will already have a // subscription and this call will be a no-op). MaybeWatchForTriggerDuringStepTransition(); } TrackedElement* InteractionSequence::GetNamedElement( const base::StringPiece& name) { const auto it = named_elements_.find(std::string(name)); TrackedElement* result = nullptr; if (it != named_elements_.end()) { result = it->second.get(); } else { NOTREACHED(); } return result; } const TrackedElement* InteractionSequence::GetNamedElement( const base::StringPiece& name) const { return const_cast(this)->GetNamedElement(name); } void InteractionSequence::OnElementShown(TrackedElement* element) { // If the element was destroyed before we got our callback, this could be // null. if (!element) return; DCHECK_EQ(StepType::kShown, next_step()->type); DCHECK(element->identifier() == next_step()->id); // Note that we don't need to look for a named element here, as any named // element referenced in a kShown step must already exist, and therefore we // should have already transitioned or failed. DoStepTransition(element); } void InteractionSequence::OnElementActivated(TrackedElement* element) { // If the element was destroyed before we got our callback, this could be // null. if (!element) return; DCHECK_EQ(StepType::kActivated, next_step()->type); DCHECK(element->identifier() == next_step()->id); if (MatchesNameIfSpecified(element, next_step()->element_name)) DoStepTransition(element); } void InteractionSequence::OnCustomEvent(TrackedElement* element) { DCHECK_EQ(StepType::kCustomEvent, next_step()->type); if (next_step()->id && next_step()->id != element->identifier()) return; if (MatchesNameIfSpecified(element, next_step()->element_name)) DoStepTransition(element); } void InteractionSequence::OnElementHidden(TrackedElement* element) { if (!started_) { DCHECK_EQ(next_step()->element, element); missing_first_element_ = true; next_step()->subscription = ElementTracker::Subscription(); next_step()->element = nullptr; return; } if (current_step_->element == element) { // If the current step is marked as needing to remain visible and we haven't // seen the triggering event for the next step, abort. if (current_step_->must_remain_visible.value() && !trigger_during_callback_) { Abort(AbortedReason::kElementHiddenDuringStep); return; } // This element pointer is no longer valid and we can stop watching. current_step_->subscription = ElementTracker::Subscription(); current_step_->element = nullptr; } // If we got a hidden callback and it wasn't to abort the current step, it // must be because we're waiting on the next step to start. if (next_step() && next_step()->id == element->identifier() && next_step()->type == StepType::kHidden) { if (next_step()->uses_named_element()) { // Find the named element; if it still exists, it hasn't been hidden. const auto it = named_elements_.find(next_step()->element_name); DCHECK(it != named_elements_.end()); if (it->second.get()) return; } // We can get this situation when an element goes away during a step // callback, before we've actually staged the following hide step. At this // point it's not valid to do a transition, so we'll mark that the next // transition has happened. if (processing_step_) { trigger_during_callback_ = true; } else { DoStepTransition(element); } } } void InteractionSequence::OnTriggerDuringStepTransition( TrackedElement* element) { if (!next_step() || !element) return; switch (next_step()->type) { case StepType::kHidden: case StepType::kActivated: case StepType::kShown: // We should know the identifier and name ahead of time for activation // steps, so just make sure nothing has gone awry. DCHECK(element->identifier() == next_step()->id); DCHECK(MatchesNameIfSpecified(element, next_step()->element_name)); break; case StepType::kCustomEvent: // Since we don't specify the element ID when registering for custom // events we have to see if we specified an ID or name and if so, whether // it matches the element we actually got. if (next_step()->id && element->identifier() != next_step()->id) return; if (!MatchesNameIfSpecified(element, next_step()->element_name)) return; break; default: NOTREACHED(); return; } // Barring disaster, we will immediately transition as soon as we finish // processing the current step. trigger_during_callback_ = true; if (next_step()->type == StepType::kHidden) { next_step()->element = nullptr; next_step()->subscription = base::CallbackListSubscription(); } else { // Since we've hit the trigger for the next step, we need to make sure we // clean up (and possibly abort) if the element goes away before we can // finish processing the current step. next_step()->element = element; next_step()->subscription = ElementTracker::GetElementTracker()->AddElementHiddenCallback( element->identifier(), element->context(), base::BindRepeating( &InteractionSequence::OnElementHiddenDuringStepTransition, base::Unretained(this))); } } void InteractionSequence::OnElementHiddenDuringStepTransition( TrackedElement* element) { if (!next_step() || element != next_step()->element) return; next_step()->element = nullptr; next_step()->subscription = ElementTracker::Subscription(); } void InteractionSequence::OnElementHiddenWaitingForActivate( TrackedElement* element) { if (!next_step()) return; if (next_step()->element == element || !ElementTracker::GetElementTracker()->GetFirstMatchingElement( next_step()->id, context())) { Abort(AbortedReason::kElementNotVisibleAtStartOfStep); } } void InteractionSequence::MaybeWatchForTriggerDuringStepTransition() { // This should only be called while we're processing a step, there is a next // step we care about, and we aren't already subscribed for an event on that // step. if (!processing_step_ || configuration_->steps.empty() || next_step()->subscription) { return; } // If the next element is named but we have not yet named it, don't add a // listener; we can add one when we name the element. TrackedElement* named_element = nullptr; if (next_step()->uses_named_element()) { const auto it = named_elements_.find(next_step()->element_name); if (it != named_elements_.end()) named_element = it->second.get(); if (!named_element) return; } // If the next step is a discrete event, listen for the event so we don't miss // it during the step callback. switch (next_step()->type) { case StepType::kActivated: // For activation events the ID of the next node must be known. if (next_step()->id) { next_step()->subscription = ElementTracker::GetElementTracker()->AddElementActivatedCallback( next_step()->id, GetElementContext(named_element), base::BindRepeating( &InteractionSequence::OnTriggerDuringStepTransition, base::Unretained((this)))); } break; case StepType::kCustomEvent: // For custom events the ID is not necessary because ElementTracker allows // just listening for the event. next_step()->subscription = ElementTracker::GetElementTracker()->AddCustomEventCallback( next_step()->custom_event_type, GetElementContext(named_element), base::BindRepeating( &InteractionSequence::OnTriggerDuringStepTransition, base::Unretained((this)))); break; case StepType::kShown: // For shown events, the ID must be known and the event need only be // observed if the state change itself is being observed or the element // might immediately become invisible again. if ((next_step()->transition_only_on_event || !next_step()->must_remain_visible.value()) && next_step()->id) { next_step()->subscription = ElementTracker::GetElementTracker()->AddElementShownCallback( next_step()->id, GetElementContext(named_element), base::BindRepeating( &InteractionSequence::OnTriggerDuringStepTransition, base::Unretained((this)))); } break; case StepType::kHidden: // For hidden events, the ID must be known. Only watch if the state change // itself is the step transition. if (next_step()->transition_only_on_event && next_step()->id) { next_step()->subscription = ElementTracker::GetElementTracker()->AddElementHiddenCallback( next_step()->id, GetElementContext(named_element), base::BindRepeating( &InteractionSequence::OnTriggerDuringStepTransition, base::Unretained((this)))); } break; } } void InteractionSequence::DoStepTransition(TrackedElement* element) { // There are a number of callbacks during this method that could potentially // result in this InteractionSequence being destructed, so maintain a weak // pointer we can check to see if we need to bail out early. base::WeakPtr delete_guard = weak_factory_.GetWeakPtr(); auto* const tracker = ElementTracker::GetElementTracker(); { // This block is non-re-entrant. DCHECK(!processing_step_); base::WeakAutoReset processing(weak_factory_.GetWeakPtr(), &InteractionSequence::processing_step_, true); // End the current step. if (current_step_) { // Unsubscribe from any events during the step-end process. Since the step // has ended, conditions like "must remain visible" no longer apply. current_step_->subscription = ElementTracker::Subscription(); RunIfValid(std::move(current_step_->end_callback), current_step_->element.get()); if (!delete_guard || AbortedDuringCallback()) return; } // Set up the new current step. current_step_ = std::move(configuration_->steps.front()); configuration_->steps.pop_front(); ++active_step_index_; DCHECK(!current_step_->element || current_step_->element == element); current_step_->element = current_step_->type == StepType::kHidden ? nullptr : element; if (current_step_->element) { current_step_->subscription = tracker->AddElementHiddenCallback( current_step_->element->identifier(), current_step_->element->context(), base::BindRepeating(&InteractionSequence::OnElementHidden, base::Unretained(this))); } else { current_step_->subscription = ElementTracker::Subscription(); } // If we've got a guard on the new current step's element having gone away // while we were waiting, we can release it. next_step_hidden_subscription_ = ElementTracker::Subscription(); // Special care must be taken here, because theoretically *anything* could // happen as a result of this callback. If the next step is a shown or // hidden step and the element becomes shown or hidden (or it's a step that // requires the element to be visible and it is not), then the appropriate // transition (or Abort()) will happen in StageNextStep() below. // // If, however, the callback *activates* or sends a custom event on the next // target element, and the next step is of the matching type, then the event // will not register unless we explicitly listen for it. This will add a // temporary callback to handle this case. MaybeWatchForTriggerDuringStepTransition(); // Start the step. Like all callbacks, this could abort the sequence, or // cause `element` to become invalid. Because of this we use the element // field of the current step from here forward, because we've installed a // callback above that will null it out if it becomes invalid. RunIfValid(std::move(current_step_->start_callback), this, current_step_->element.get()); if (!delete_guard || AbortedDuringCallback()) return; } if (configuration_->steps.empty()) { // Reset anything that might cause state change during the final callback. // After this, Abort() will have basically no effect, since by the time it // gets called, both the aborted and step end callbacks will be null. current_step_->subscription = ElementTracker::Subscription(); configuration_->aborted_callback.Reset(); // Last step end callback needs to be run before sequence completed. // Because the InteractionSequence could conceivably be destroyed during // one of these callbacks, make local copies of the callbacks and data. base::OnceClosure quit_closure = std::move(quit_run_loop_closure_for_testing_); CompletedCallback completed_callback = std::move(configuration_->completed_callback); std::unique_ptr last_step = std::move(current_step_); RunIfValid(std::move(last_step->end_callback), last_step->element.get()); RunIfValid(std::move(completed_callback)); RunIfValid(std::move(quit_closure)); return; } // Since we're not done, load up the next step. StageNextStep(); } void InteractionSequence::StageNextStep() { auto* const tracker = ElementTracker::GetElementTracker(); Step* const next = next_step(); // Note that if the target element for the next step was activated and then // hidden during the previous step transition, `next_element` could be null. TrackedElement* next_element; if (trigger_during_callback_ || next->element) { next_element = next->element; } else if (next->uses_named_element()) { next->element = GetNamedElement(next->element_name); next_element = next->element; // We should have set the ID on this step when the element was named; the // element may have gone away but shouldn't differ in ID from what we // previously recorded. DCHECK(!next_element || next->id == next_element->identifier()); } else { next_element = tracker->GetFirstMatchingElement(next->id, context()); if (!next_element && next->any_context) next_element = tracker->GetElementInAnyContext(next->id); } if (!trigger_during_callback_ && next->must_be_visible.value() && !next_element) { // We're going to abort, but we have to finish the current step first. if (current_step_) { RunIfValid(std::move(current_step_->end_callback), current_step_->element.get()); } Abort(AbortedReason::kElementNotVisibleAtStartOfStep); return; } switch (next->type) { case StepType::kShown: if (trigger_during_callback_) { trigger_during_callback_ = false; if (next->must_remain_visible.value() && !next_element) { Abort(AbortedReason::kElementHiddenDuringStep); return; } else { DoStepTransition(next_element); } } else if (next_element && !next->transition_only_on_event) { DoStepTransition(next_element); } else { DCHECK(!next->uses_named_element()); auto callback = base::BindRepeating( &InteractionSequence::OnElementShown, base::Unretained(this)); next->subscription = next->any_context ? tracker->AddElementShownInAnyContextCallback( next->id, callback) : tracker->AddElementShownCallback( next->id, context(), callback); } break; case StepType::kHidden: if (trigger_during_callback_ || (!next_element && !next->transition_only_on_event)) { trigger_during_callback_ = false; DoStepTransition(nullptr); } else { DCHECK(next_element || !next->uses_named_element()); next->subscription = tracker->AddElementHiddenCallback( next->id, context(), base::BindRepeating(&InteractionSequence::OnElementHidden, base::Unretained(this))); } break; case StepType::kActivated: if (trigger_during_callback_) { trigger_during_callback_ = false; DoStepTransition(next_element); } else { DCHECK(next_element || !next->uses_named_element()); const ElementContext target_context = GetElementContext(next_element); next->subscription = tracker->AddElementActivatedCallback( next->id, target_context, base::BindRepeating(&InteractionSequence::OnElementActivated, base::Unretained(this))); // It's possible to have the element hidden between the time we stage // the event and when the activation would actually come in (which // could be never). In this case, we should abort. if (next_step()->must_be_visible.value()) { next_step_hidden_subscription_ = tracker->AddElementHiddenCallback( next->id, target_context, base::BindRepeating( &InteractionSequence::OnElementHiddenWaitingForActivate, base::Unretained(this))); } } break; case StepType::kCustomEvent: if (trigger_during_callback_) { trigger_during_callback_ = false; DoStepTransition(next_element); } else { DCHECK(next_element || !next->uses_named_element()); const ElementContext target_context = GetElementContext(next_element); next->subscription = tracker->AddCustomEventCallback( next->custom_event_type, target_context, base::BindRepeating(&InteractionSequence::OnCustomEvent, base::Unretained(this))); // It's possible to have the element hidden between the time we stage // the event and when the custom event would actually come in (which // could be never). In this case, we should abort. if (next_step()->must_be_visible.value()) { DCHECK(next->id); next_step_hidden_subscription_ = tracker->AddElementHiddenCallback( next->id, target_context, base::BindRepeating( &InteractionSequence::OnElementHiddenWaitingForActivate, base::Unretained(this))); } } break; } } void InteractionSequence::Abort(AbortedReason reason) { DCHECK(started_); next_step_hidden_subscription_ = ElementTracker::Subscription(); // The entire InteractionSequence could also go away during a callback, so // save anything we need locally so that we don't have to access any class // members as we finish terminating the sequence. base::OnceClosure quit_closure = std::move(quit_run_loop_closure_for_testing_); std::unique_ptr current_step = std::move(current_step_); AbortedCallback aborted_callback = std::move(configuration_->aborted_callback); int active_step_index = active_step_index_; StepType target_step_type = StepType::kShown; ElementIdentifier target_id; // The element could go away independently of the sequence. SafeElementReference target_element; if (reason == AbortedReason::kElementNotVisibleAtStartOfStep || reason == AbortedReason::kElementHiddenBeforeSequenceStart) { ++active_step_index; if (next_step()) { target_step_type = next_step()->type; target_id = next_step()->id; } } else if (current_step) { target_step_type = current_step->type; target_id = current_step->id; target_element = SafeElementReference(current_step->element); } configuration_->steps.clear(); // Note that if the sequence has already been aborted, this is a no-op, the // callbacks will already be null. if (current_step) { // Stop listening for events; we don't want additional callbacks during // teardown. current_step->subscription = ElementTracker::Subscription(); RunIfValid(std::move(current_step->end_callback), current_step->element); } RunIfValid(std::move(aborted_callback), active_step_index, target_element.get(), target_id, target_step_type, reason); RunIfValid(std::move(quit_closure)); } bool InteractionSequence::AbortedDuringCallback() const { // All step callbacks are sourced from the current step. If the current step // is null, then the sequence must have aborted (which clears out the current // step). Completion can only happen after step callbacks are finished if (current_step_) return false; DCHECK(configuration_->steps.empty()); DCHECK(!configuration_->aborted_callback); return true; } bool InteractionSequence::MatchesNameIfSpecified( const TrackedElement* element, const base::StringPiece& name) const { if (name.empty()) return true; const TrackedElement* const expected = GetNamedElement(name); DCHECK(expected); return element == expected; } InteractionSequence::Step* InteractionSequence::next_step() { return configuration_->steps.empty() ? nullptr : configuration_->steps.front().get(); } ElementContext InteractionSequence::context() const { return configuration_->context; } ElementContext InteractionSequence::GetElementContext( const TrackedElement* element) const { return element ? element->context() : context(); } void PrintTo(InteractionSequence::StepType step_type, std::ostream* os) { const char* const kStepTypeNames[] = { "StepType::kShown", "StepType::kActivated", "StepType::kHidden", "StepType::kCustomEvent"}; constexpr int kCount = sizeof(kStepTypeNames) / sizeof(kStepTypeNames[0]); static_assert(kCount == static_cast(InteractionSequence::StepType::kMaxValue) + 1); const int value = static_cast(step_type); *os << ((value < 0 || value >= kCount) ? "[invalid StepType]" : kStepTypeNames[value]); } void PrintTo(InteractionSequence::AbortedReason reason, std::ostream* os) { const char* const kAbortedReasonNames[] = { "AbortedReason::kSequenceDestroyed", "AbortedReason::kElementHiddenBeforeSequenceStart", "AbortedReason::kElementNotVisibleAtStartOfStep", "AbortedReason::kElementHiddenDuringStep"}; constexpr int kCount = sizeof(kAbortedReasonNames) / sizeof(kAbortedReasonNames[0]); static_assert( kCount == static_cast(InteractionSequence::AbortedReason::kMaxValue) + 1); const int value = static_cast(reason); *os << ((value < 0 || value >= kCount) ? "[invalid StepType]" : kAbortedReasonNames[value]); } extern std::ostream& operator<<(std::ostream& os, InteractionSequence::StepType step_type) { PrintTo(step_type, &os); return os; } extern std::ostream& operator<<(std::ostream& os, InteractionSequence::AbortedReason reason) { PrintTo(reason, &os); return os; } } // namespace ui