diff --git a/.mapping.json b/.mapping.json index c42633888..8dea46eeb 100644 --- a/.mapping.json +++ b/.mapping.json @@ -1111,6 +1111,7 @@ "client/android/div/src/main/java/com/yandex/div/core/view2/ShadowCache.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/view2/ShadowCache.kt", "client/android/div/src/main/java/com/yandex/div/core/view2/SightActionIsEnabledObserver.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/view2/SightActionIsEnabledObserver.kt", "client/android/div/src/main/java/com/yandex/div/core/view2/ViewBindingProvider.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/view2/ViewBindingProvider.kt", + "client/android/div/src/main/java/com/yandex/div/core/view2/ViewLocator.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/view2/ViewLocator.kt", "client/android/div/src/main/java/com/yandex/div/core/view2/ViewVisibilityCalculator.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/view2/ViewVisibilityCalculator.kt", "client/android/div/src/main/java/com/yandex/div/core/view2/animations/DivComparator.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/view2/animations/DivComparator.kt", "client/android/div/src/main/java/com/yandex/div/core/view2/animations/DivComparatorReporter.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/view2/animations/DivComparatorReporter.kt", @@ -1460,6 +1461,7 @@ "client/android/divkit-demo-app/lint-baseline.xml":"divkit/public/client/android/divkit-demo-app/lint-baseline.xml", "client/android/divkit-demo-app/proguard-rules.pro":"divkit/public/client/android/divkit-demo-app/proguard-rules.pro", "client/android/divkit-demo-app/src/androidTest/java/com/yandex/div/AtStartHistogramsTest.kt":"divkit/public/client/android/divkit-demo-app/src/androidTest/java/com/yandex/div/AtStartHistogramsTest.kt", + "client/android/divkit-demo-app/src/androidTest/java/com/yandex/div/DivActionTest.kt":"divkit/public/client/android/divkit-demo-app/src/androidTest/java/com/yandex/div/DivActionTest.kt", "client/android/divkit-demo-app/src/androidTest/java/com/yandex/div/DivCollectionAdapterTest.kt":"divkit/public/client/android/divkit-demo-app/src/androidTest/java/com/yandex/div/DivCollectionAdapterTest.kt", "client/android/divkit-demo-app/src/androidTest/java/com/yandex/div/DivCustomTest.kt":"divkit/public/client/android/divkit-demo-app/src/androidTest/java/com/yandex/div/DivCustomTest.kt", "client/android/divkit-demo-app/src/androidTest/java/com/yandex/div/DivFocusableInputTest.kt":"divkit/public/client/android/divkit-demo-app/src/androidTest/java/com/yandex/div/DivFocusableInputTest.kt", @@ -18072,6 +18074,9 @@ "test_data/tutorials/counter.json":"divkit/public/test_data/tutorials/counter.json", "test_data/tutorials/stateful_card.json":"divkit/public/test_data/tutorials/stateful_card.json", "test_data/tutorials/ticking_timer.json":"divkit/public/test_data/tutorials/ticking_timer.json", + "test_data/ui_test_data/actions/scoped_actions.json":"divkit/public/test_data/ui_test_data/actions/scoped_actions.json", + "test_data/ui_test_data/actions/scoped_actions_ambiguous_scope.json":"divkit/public/test_data/ui_test_data/actions/scoped_actions_ambiguous_scope.json", + "test_data/ui_test_data/actions/scoped_actions_nested_scope.json":"divkit/public/test_data/ui_test_data/actions/scoped_actions_nested_scope.json", "test_data/ui_test_data/custom/div_custom_several_states_changing.json":"divkit/public/test_data/ui_test_data/custom/div_custom_several_states_changing.json", "test_data/ui_test_data/custom/div_custom_state_changing.json":"divkit/public/test_data/ui_test_data/custom/div_custom_state_changing.json", "test_data/ui_test_data/focus/actions.json":"divkit/public/test_data/ui_test_data/focus/actions.json", diff --git a/client/android/div/src/main/java/com/yandex/div/core/DivActionHandler.java b/client/android/div/src/main/java/com/yandex/div/core/DivActionHandler.java index 4b96b46db..a3b5c6297 100644 --- a/client/android/div/src/main/java/com/yandex/div/core/DivActionHandler.java +++ b/client/android/div/src/main/java/com/yandex/div/core/DivActionHandler.java @@ -1,17 +1,20 @@ package com.yandex.div.core; import android.net.Uri; +import android.view.View; import androidx.annotation.CallSuper; import androidx.annotation.NonNull; import androidx.annotation.Nullable; - import com.yandex.div.core.actions.DivActionTypedHandlerProxy; import com.yandex.div.core.annotations.PublicApi; import com.yandex.div.core.downloader.DivDownloadActionHandler; import com.yandex.div.core.expression.storedvalues.StoredValuesActionHandler; import com.yandex.div.core.state.DivStatePath; import com.yandex.div.core.state.PathFormatException; +import com.yandex.div.core.view2.BindingContext; import com.yandex.div.core.view2.Div2View; +import com.yandex.div.core.view2.ViewLocator; +import com.yandex.div.core.view2.divs.widgets.DivHolderView; import com.yandex.div.core.view2.items.DivItemChangeActionHandler; import com.yandex.div.data.VariableMutationException; import com.yandex.div.internal.Assert; @@ -43,6 +46,8 @@ public class DivActionHandler { public static final String TIMER = "timer"; public static final String TRIGGER = "trigger"; public static final String VIDEO = "video"; + public static final String ANIMATION_END = "animation_end"; + public static final String ANIMATION_CANCEL = "animation_cancel"; } private static final String SCHEME_DIV_ACTION = "div-action"; @@ -95,14 +100,17 @@ public class DivActionHandler { @NonNull DivViewFacade view, @NonNull ExpressionResolver resolver ) { - if (DivActionTypedHandlerProxy.handleAction(action, view, resolver)) { + ExpressionResolver scopedResolver = findExpressionResolverById((Div2View) view, action.scopeId); + ExpressionResolver localResolver = scopedResolver == null ? resolver : scopedResolver; + + if (DivActionTypedHandlerProxy.handleAction(action, view, localResolver)) { return true; } Uri url = action.url != null ? action.url.evaluate(resolver) : null; if (DivDownloadActionHandler.canHandle(url, view)) { - return DivDownloadActionHandler.handleAction(action, (Div2View) view, resolver); + return DivDownloadActionHandler.handleAction(action, (Div2View) view, localResolver); } - return handleActionUrl(url, view, resolver); + return handleAction(action.scopeId, url, view, localResolver); } /** @@ -220,14 +228,17 @@ public class DivActionHandler { @NonNull DivViewFacade view, @NonNull ExpressionResolver resolver ) { - if (DivActionTypedHandlerProxy.handleVisibilityAction(action, view, resolver)) { + ExpressionResolver scopedResolver = findExpressionResolverById((Div2View) view, action.getScopeId()); + ExpressionResolver localResolver = scopedResolver == null ? resolver : scopedResolver; + + if (DivActionTypedHandlerProxy.handleVisibilityAction(action, view, localResolver)) { return true; } Uri url = action.getUrl() != null ? action.getUrl().evaluate(resolver) : null; if (DivDownloadActionHandler.canHandle(url, view)) { - return DivDownloadActionHandler.handleVisibilityAction(action, (Div2View) view, resolver); + return DivDownloadActionHandler.handleVisibilityAction(action, (Div2View) view, localResolver); } - return handleActionUrl(url, view, resolver); + return handleAction(action.getScopeId(), url, view, resolver); } /** @@ -325,6 +336,35 @@ public class DivActionHandler { @Nullable Uri uri, @NonNull DivViewFacade view, @NonNull ExpressionResolver resolver + ) { + return handleActionUrl(null, uri, view, resolver); + } + + /** + * Handles the URI with {@code div-action} scheme. + * + * @param scopeId id of div that denotes scope of given action + * @param uri URI to handle + * @param view calling DivView + * @param resolver resolver for current action + * @return TRUE if uri was handled + */ + public final boolean handleActionUrl( + @Nullable String scopeId, + @Nullable Uri uri, + @NonNull DivViewFacade view, + @NonNull ExpressionResolver resolver + ) { + ExpressionResolver scopedResolver = findExpressionResolverById((Div2View) view, scopeId); + ExpressionResolver localResolver = scopedResolver == null ? resolver : scopedResolver; + return handleAction(scopeId, uri, view, localResolver); + } + + private boolean handleAction( + @Nullable String scopeId, + @Nullable Uri uri, + @NonNull DivViewFacade view, + @NonNull ExpressionResolver resolver ) { if (uri == null) { return false; @@ -332,13 +372,18 @@ public class DivActionHandler { //noinspection SimplifiableIfStatement if (SCHEME_DIV_ACTION.equals(uri.getScheme())) { - return handleAction(uri, view, resolver); + return handleActionInternal(scopeId, uri, view, resolver); } return false; } - private boolean handleAction(@NonNull Uri uri, @NonNull DivViewFacade view, @NonNull ExpressionResolver resolver) { + private boolean handleActionInternal( + @Nullable String scopeId, + @Nullable Uri uri, + @NonNull DivViewFacade view, + @NonNull ExpressionResolver resolver + ) { String action = uri.getAuthority(); if (AUTHORITY_SWITCH_STATE.equals(action)) { String stateId = uri.getQueryParameter(PARAM_STATE_ID); @@ -447,4 +492,20 @@ public class DivActionHandler { return false; } + + @Nullable + private static ExpressionResolver findExpressionResolverById(Div2View divView, @Nullable String id) { + if (id == null) { + return null; + } + + View targetView = ViewLocator.findViewWithTag(divView, id); + if (targetView instanceof DivHolderView) { + BindingContext bindingContext = ((DivHolderView) targetView).getBindingContext(); + if (bindingContext != null) { + return bindingContext.getExpressionResolver(); + } + } + return null; + } } diff --git a/client/android/div/src/main/java/com/yandex/div/core/actions/DivActionTypedArrayMutationHandler.kt b/client/android/div/src/main/java/com/yandex/div/core/actions/DivActionTypedArrayMutationHandler.kt index 819c3883e..a6ce473fd 100644 --- a/client/android/div/src/main/java/com/yandex/div/core/actions/DivActionTypedArrayMutationHandler.kt +++ b/client/android/div/src/main/java/com/yandex/div/core/actions/DivActionTypedArrayMutationHandler.kt @@ -18,6 +18,7 @@ internal class DivActionTypedArrayMutationHandler @Inject constructor() : DivActionTypedHandler { override fun handleAction( + scopeId: String?, action: DivActionTyped, view: Div2View, resolver: ExpressionResolver, diff --git a/client/android/div/src/main/java/com/yandex/div/core/actions/DivActionTypedClearFocusHandler.kt b/client/android/div/src/main/java/com/yandex/div/core/actions/DivActionTypedClearFocusHandler.kt index 7249f75be..b54c12b43 100644 --- a/client/android/div/src/main/java/com/yandex/div/core/actions/DivActionTypedClearFocusHandler.kt +++ b/client/android/div/src/main/java/com/yandex/div/core/actions/DivActionTypedClearFocusHandler.kt @@ -8,7 +8,13 @@ import javax.inject.Singleton @Singleton internal class DivActionTypedClearFocusHandler @Inject constructor() : DivActionTypedHandler { - override fun handleAction(action: DivActionTyped, view: Div2View, resolver: ExpressionResolver): Boolean { + + override fun handleAction( + scopeId: String?, + action: DivActionTyped, + view: Div2View, + resolver: ExpressionResolver + ): Boolean { return when (action) { is DivActionTyped.ClearFocus -> { view.clearFocus() diff --git a/client/android/div/src/main/java/com/yandex/div/core/actions/DivActionTypedCopyToClipboardHandler.kt b/client/android/div/src/main/java/com/yandex/div/core/actions/DivActionTypedCopyToClipboardHandler.kt index 11e415c1b..db5904c83 100644 --- a/client/android/div/src/main/java/com/yandex/div/core/actions/DivActionTypedCopyToClipboardHandler.kt +++ b/client/android/div/src/main/java/com/yandex/div/core/actions/DivActionTypedCopyToClipboardHandler.kt @@ -17,6 +17,7 @@ internal class DivActionTypedCopyToClipboardHandler @Inject constructor() : DivActionTypedHandler { override fun handleAction( + scopeId: String?, action: DivActionTyped, view: Div2View, resolver: ExpressionResolver, diff --git a/client/android/div/src/main/java/com/yandex/div/core/actions/DivActionTypedDictSetValueHandler.kt b/client/android/div/src/main/java/com/yandex/div/core/actions/DivActionTypedDictSetValueHandler.kt index 10e57b8d1..78f9a531b 100644 --- a/client/android/div/src/main/java/com/yandex/div/core/actions/DivActionTypedDictSetValueHandler.kt +++ b/client/android/div/src/main/java/com/yandex/div/core/actions/DivActionTypedDictSetValueHandler.kt @@ -15,6 +15,7 @@ internal class DivActionTypedDictSetValueHandler @Inject constructor() : DivActionTypedHandler { override fun handleAction( + scopeId: String?, action: DivActionTyped, view: Div2View, resolver: ExpressionResolver, diff --git a/client/android/div/src/main/java/com/yandex/div/core/actions/DivActionTypedFocusElementHandler.kt b/client/android/div/src/main/java/com/yandex/div/core/actions/DivActionTypedFocusElementHandler.kt index 429702c2d..c318dff03 100644 --- a/client/android/div/src/main/java/com/yandex/div/core/actions/DivActionTypedFocusElementHandler.kt +++ b/client/android/div/src/main/java/com/yandex/div/core/actions/DivActionTypedFocusElementHandler.kt @@ -12,7 +12,13 @@ import javax.inject.Singleton @Singleton internal class DivActionTypedFocusElementHandler @Inject constructor() : DivActionTypedHandler { - override fun handleAction(action: DivActionTyped, view: Div2View, resolver: ExpressionResolver): Boolean { + + override fun handleAction( + scopeId: String?, + action: DivActionTyped, + view: Div2View, + resolver: ExpressionResolver + ): Boolean { return when (action) { is DivActionTyped.FocusElement -> { handleRequestFocus(action.value, view, resolver) diff --git a/client/android/div/src/main/java/com/yandex/div/core/actions/DivActionTypedHandler.kt b/client/android/div/src/main/java/com/yandex/div/core/actions/DivActionTypedHandler.kt index 08330043b..dfb41607d 100644 --- a/client/android/div/src/main/java/com/yandex/div/core/actions/DivActionTypedHandler.kt +++ b/client/android/div/src/main/java/com/yandex/div/core/actions/DivActionTypedHandler.kt @@ -6,5 +6,5 @@ import com.yandex.div2.DivActionTyped internal interface DivActionTypedHandler { - fun handleAction(action: DivActionTyped, view: Div2View, resolver: ExpressionResolver): Boolean + fun handleAction(scopeId: String?, action: DivActionTyped, view: Div2View, resolver: ExpressionResolver): Boolean } diff --git a/client/android/div/src/main/java/com/yandex/div/core/actions/DivActionTypedHandlerCombiner.kt b/client/android/div/src/main/java/com/yandex/div/core/actions/DivActionTypedHandlerCombiner.kt index 3fda23025..603588e10 100644 --- a/client/android/div/src/main/java/com/yandex/div/core/actions/DivActionTypedHandlerCombiner.kt +++ b/client/android/div/src/main/java/com/yandex/div/core/actions/DivActionTypedHandlerCombiner.kt @@ -12,8 +12,13 @@ internal class DivActionTypedHandlerCombiner @Inject constructor( private val handlers: Set<@JvmSuppressWildcards DivActionTypedHandler> ) { - fun handleAction(action: DivActionTyped, div2View: Div2View, resolver: ExpressionResolver): Boolean { - val wasHandled = handlers.find { it.handleAction(action, div2View, resolver) } != null + fun handleAction( + scopeId: String?, + action: DivActionTyped, + div2View: Div2View, + resolver: ExpressionResolver + ): Boolean { + val wasHandled = handlers.find { it.handleAction(scopeId, action, div2View, resolver) } != null if (!wasHandled) { KLog.d(TAG) { "Unexpected ${action::class.java} was not handled" } } diff --git a/client/android/div/src/main/java/com/yandex/div/core/actions/DivActionTypedHandlerProxy.kt b/client/android/div/src/main/java/com/yandex/div/core/actions/DivActionTypedHandlerProxy.kt index 8cb54d411..b7e1cd330 100644 --- a/client/android/div/src/main/java/com/yandex/div/core/actions/DivActionTypedHandlerProxy.kt +++ b/client/android/div/src/main/java/com/yandex/div/core/actions/DivActionTypedHandlerProxy.kt @@ -14,15 +14,16 @@ internal object DivActionTypedHandlerProxy { @JvmStatic fun handleVisibilityAction(action: DivSightAction, view: DivViewFacade, resolver: ExpressionResolver): Boolean { - return handleAction(action.typed, view, resolver, action.downloadCallbacks) + return handleAction(action.scopeId, action.typed, view, resolver, action.downloadCallbacks) } @JvmStatic fun handleAction(action: DivAction, view: DivViewFacade, resolver: ExpressionResolver): Boolean { - return handleAction(action.typed, view, resolver, action.downloadCallbacks) + return handleAction(action.scopeId, action.typed, view, resolver, action.downloadCallbacks) } private fun handleAction( + scopeId: String?, action: DivActionTyped?, view: DivViewFacade, resolver: ExpressionResolver, @@ -38,6 +39,7 @@ internal object DivActionTypedHandlerProxy { if (action is DivActionTyped.Download) { return DivDownloadActionHandler.handleAction(action.value, downloadCallbacks, view, resolver) } - return view.div2Component.actionTypedHandlerCombiner.handleAction(action, view, resolver) + return view.div2Component.actionTypedHandlerCombiner.handleAction(scopeId, action, view, resolver) + } } diff --git a/client/android/div/src/main/java/com/yandex/div/core/actions/DivActionTypedHideTooltipHandler.kt b/client/android/div/src/main/java/com/yandex/div/core/actions/DivActionTypedHideTooltipHandler.kt index f8bdbb29a..24e876ce6 100644 --- a/client/android/div/src/main/java/com/yandex/div/core/actions/DivActionTypedHideTooltipHandler.kt +++ b/client/android/div/src/main/java/com/yandex/div/core/actions/DivActionTypedHideTooltipHandler.kt @@ -8,10 +8,11 @@ import javax.inject.Inject import javax.inject.Singleton @Singleton -class DivActionTypedHideTooltipHandler @Inject constructor() +internal class DivActionTypedHideTooltipHandler @Inject constructor() : DivActionTypedHandler { override fun handleAction( + scopeId: String?, action: DivActionTyped, view: Div2View, resolver: ExpressionResolver diff --git a/client/android/div/src/main/java/com/yandex/div/core/actions/DivActionTypedSetStateHandler.kt b/client/android/div/src/main/java/com/yandex/div/core/actions/DivActionTypedSetStateHandler.kt index b2d0c3ff3..a6bf36952 100644 --- a/client/android/div/src/main/java/com/yandex/div/core/actions/DivActionTypedSetStateHandler.kt +++ b/client/android/div/src/main/java/com/yandex/div/core/actions/DivActionTypedSetStateHandler.kt @@ -14,6 +14,7 @@ internal class DivActionTypedSetStateHandler @Inject constructor() : DivActionTypedHandler { override fun handleAction( + scopeId: String?, action: DivActionTyped, view: Div2View, resolver: ExpressionResolver diff --git a/client/android/div/src/main/java/com/yandex/div/core/actions/DivActionTypedSetVariableHandler.kt b/client/android/div/src/main/java/com/yandex/div/core/actions/DivActionTypedSetVariableHandler.kt index e663ac107..29e1cc9df 100644 --- a/client/android/div/src/main/java/com/yandex/div/core/actions/DivActionTypedSetVariableHandler.kt +++ b/client/android/div/src/main/java/com/yandex/div/core/actions/DivActionTypedSetVariableHandler.kt @@ -17,6 +17,7 @@ internal class DivActionTypedSetVariableHandler @Inject constructor() : DivActionTypedHandler { override fun handleAction( + scopeId: String?, action: DivActionTyped, view: Div2View, resolver: ExpressionResolver, diff --git a/client/android/div/src/main/java/com/yandex/div/core/actions/DivActionTypedShowTooltipHandler.kt b/client/android/div/src/main/java/com/yandex/div/core/actions/DivActionTypedShowTooltipHandler.kt index 8d2cecef7..888a2125e 100644 --- a/client/android/div/src/main/java/com/yandex/div/core/actions/DivActionTypedShowTooltipHandler.kt +++ b/client/android/div/src/main/java/com/yandex/div/core/actions/DivActionTypedShowTooltipHandler.kt @@ -8,10 +8,11 @@ import javax.inject.Inject import javax.inject.Singleton @Singleton -class DivActionTypedShowTooltipHandler @Inject constructor() +internal class DivActionTypedShowTooltipHandler @Inject constructor() : DivActionTypedHandler { override fun handleAction( + scopeId: String?, action: DivActionTyped, view: Div2View, resolver: ExpressionResolver diff --git a/client/android/div/src/main/java/com/yandex/div/core/actions/DivActionTypedTimerHandler.kt b/client/android/div/src/main/java/com/yandex/div/core/actions/DivActionTypedTimerHandler.kt index 4ac1990c8..3350d1c8b 100644 --- a/client/android/div/src/main/java/com/yandex/div/core/actions/DivActionTypedTimerHandler.kt +++ b/client/android/div/src/main/java/com/yandex/div/core/actions/DivActionTypedTimerHandler.kt @@ -8,9 +8,10 @@ import javax.inject.Inject import javax.inject.Singleton @Singleton -class DivActionTypedTimerHandler @Inject constructor() +internal class DivActionTypedTimerHandler @Inject constructor() : DivActionTypedHandler { override fun handleAction( + scopeId: String?, action: DivActionTyped, view: Div2View, resolver: ExpressionResolver diff --git a/client/android/div/src/main/java/com/yandex/div/core/actions/DivActionTypedVideoHandler.kt b/client/android/div/src/main/java/com/yandex/div/core/actions/DivActionTypedVideoHandler.kt index 8d7724417..cc83dfa40 100644 --- a/client/android/div/src/main/java/com/yandex/div/core/actions/DivActionTypedVideoHandler.kt +++ b/client/android/div/src/main/java/com/yandex/div/core/actions/DivActionTypedVideoHandler.kt @@ -8,10 +8,11 @@ import javax.inject.Inject import javax.inject.Singleton @Singleton -class DivActionTypedVideoHandler @Inject constructor() +internal class DivActionTypedVideoHandler @Inject constructor() : DivActionTypedHandler { override fun handleAction( + scopeId: String?, action: DivActionTyped, view: Div2View, resolver: ExpressionResolver diff --git a/client/android/div/src/main/java/com/yandex/div/core/view2/ViewLocator.kt b/client/android/div/src/main/java/com/yandex/div/core/view2/ViewLocator.kt new file mode 100644 index 000000000..6ec19c334 --- /dev/null +++ b/client/android/div/src/main/java/com/yandex/div/core/view2/ViewLocator.kt @@ -0,0 +1,41 @@ +package com.yandex.div.core.view2 + +import android.view.View +import android.view.ViewGroup +import com.yandex.div.core.actions.logError + +internal object ViewLocator { + + @JvmStatic + fun findViewWithTag(divView: Div2View, tag: String): View? { + val foundViews = divView.view.findViewsWithTag(tag) + if (foundViews.isEmpty()) { + return null + } + if (foundViews.size > 1) { + divView.logError(RuntimeException("Ambiguous scope id. There are ${foundViews.size} divs with id '$tag'")) + return null + } + return foundViews.first() + } + + private fun View.findViewsWithTag(tag: Any?): List { + if (tag == null) return emptyList() + val result = mutableListOf() + findViewsWithTagTraversal(this, tag, result) + return result + } + + private fun findViewsWithTagTraversal(view: View, tag: Any, views: MutableList): List { + if (tag == view.tag) { + views += view + } + + if (view is ViewGroup) { + for (i in 0 until view.childCount) { + findViewsWithTagTraversal(view.getChildAt(i), tag, views) + } + } + return views + } +} diff --git a/client/android/divkit-demo-app/src/androidTest/java/com/yandex/div/DivActionTest.kt b/client/android/divkit-demo-app/src/androidTest/java/com/yandex/div/DivActionTest.kt new file mode 100644 index 000000000..e6d8ce8c5 --- /dev/null +++ b/client/android/divkit-demo-app/src/androidTest/java/com/yandex/div/DivActionTest.kt @@ -0,0 +1,95 @@ +package com.yandex.div + +import android.view.ViewGroup.LayoutParams.MATCH_PARENT +import android.view.ViewGroup.LayoutParams.WRAP_CONTENT +import androidx.test.espresso.IdlingRegistry +import androidx.test.espresso.IdlingResource +import androidx.test.rule.ActivityTestRule +import com.yandex.div.rule.uiTestRule +import com.yandex.div.steps.DivViewInteractions.viewWithTag +import com.yandex.div.steps.DivViewInteractions.viewWithTagAndText +import com.yandex.div.steps.divView +import com.yandex.div.view.isDisplayed +import com.yandex.divkit.demo.DummyActivity +import com.yandex.divkit.demo.div.DemoDiv2Logger +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test + +class DivActionTest { + + private val activityRule = ActivityTestRule(DummyActivity::class.java) + + @get:Rule + val rule = uiTestRule { activityRule } + + @Before + fun clearLogs() { + DemoDiv2Logger.clearLogActions() + } + + @After + fun unregisterAllResources() { + val resources: Collection = IdlingRegistry.getInstance().resources + if (!resources.isEmpty()) { + resources.forEach { IdlingRegistry.getInstance().unregister(it) } + } + } + + @Test + fun scopedActionChangesLocalVariable() { + divView { + testAsset = "ui_test_data/actions/scoped_actions.json" + activityRule.buildContainer(MATCH_PARENT, WRAP_CONTENT) + + tapOnText("Scoped") + + assert { + viewWithTag(tag = "title").checkHasText(text = "Title has been changed") + } + } + } + + @Test + fun notScopedActionDoesNotChangeLocalVariable() { + divView { + testAsset = "ui_test_data/actions/scoped_actions.json" + activityRule.buildContainer(MATCH_PARENT, WRAP_CONTENT) + + tapOnText("Not scoped") + + assert { + viewWithTag(tag = "title").checkHasText(text = "Lorem ipsum") + } + } + } + + @Test + fun scopedActionChangesLocalVariable_fromNestedScope() { + divView { + testAsset = "ui_test_data/actions/scoped_actions_nested_scope.json" + activityRule.buildContainer(MATCH_PARENT, WRAP_CONTENT) + + tapOnText("Content scope") + + assert { + viewWithTag(tag = "title").checkHasText(text = "Title has been changed from 'content' scope") + } + } + } + + @Test + fun scopedActionDoesNotChangeLocalVariable_whenScopeIsAmbiguous() { + divView { + testAsset = "ui_test_data/actions/scoped_actions_ambiguous_scope.json" + activityRule.buildContainer(MATCH_PARENT, WRAP_CONTENT) + + tapOnText("Ambiguous scope") + + assert { + viewWithTagAndText(tag = "some_text", text = "Lorem ipsum").isDisplayed() + } + } + } +} diff --git a/client/android/divkit-demo-app/src/androidTest/java/com/yandex/div/steps/DivViewSteps.kt b/client/android/divkit-demo-app/src/androidTest/java/com/yandex/div/steps/DivViewSteps.kt index 06a4cb206..938b7ce08 100644 --- a/client/android/divkit-demo-app/src/androidTest/java/com/yandex/div/steps/DivViewSteps.kt +++ b/client/android/divkit-demo-app/src/androidTest/java/com/yandex/div/steps/DivViewSteps.kt @@ -1,8 +1,21 @@ package com.yandex.div.steps import android.view.ViewGroup +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.ViewInteraction +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.assertion.ViewAssertions +import androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom +import androidx.test.espresso.matcher.ViewMatchers.withTagValue +import androidx.test.espresso.matcher.ViewMatchers.withText import com.yandex.div.core.Div2Context +import com.yandex.div.core.view2.Div2View +import com.yandex.test.util.Report.step import com.yandex.test.util.StepsDsl +import org.hamcrest.Matchers.allOf +import org.hamcrest.Matchers.`is` + +private val divViewMatcher = isAssignableFrom(Div2View::class.java) internal fun divView(block: DivViewSteps.() -> Unit) = block(DivViewSteps()) @@ -28,4 +41,29 @@ class DivViewSteps: DivTestAssetSteps() { fun detachFromParent() = runOnMainSync { container.removeView(div2View) } + + fun tapOnText(text: String): Unit = step("Click on text '$text'") { + onView(withText(text)).perform(click()) + } + + fun assert(f: DivViewAssertions.() -> Unit) = f(DivViewAssertions()) +} + +object DivViewInteractions { + + fun viewWithTag(tag: String): ViewInteraction { + return onView(withTagValue(`is`(tag))) + } + + fun viewWithTagAndText(tag: String, text: String): ViewInteraction { + return onView(allOf(withTagValue(`is`(tag)), withText(text))) + } +} + +@StepsDsl +class DivViewAssertions { + + fun ViewInteraction.checkHasText(text: String): Unit = step("Assert view has text '$text'") { + check(ViewAssertions.matches(withText(text))) + } } diff --git a/test_data/ui_test_data/actions/scoped_actions.json b/test_data/ui_test_data/actions/scoped_actions.json new file mode 100644 index 000000000..c8b4448bf --- /dev/null +++ b/test_data/ui_test_data/actions/scoped_actions.json @@ -0,0 +1,133 @@ +{ + "templates": { + "title": { + "type": "text", + "font_size": 24, + "font_weight": "bold", + "text_alignment_horizontal": "center", + "text_alignment_vertical": "center", + "text_color": "#586E75" + }, + "base_text": { + "type": "text", + "font_size": 18, + "font_weight": "medium", + "text_color": "#002B36" + }, + "button": { + "type": "text", + "height": { + "type": "fixed", + "value": 48 + }, + "paddings": { + "left": 8, + "top": 8, + "right": 8, + "bottom": 8 + }, + "background": [ + { + "type": "solid", + "$color": "background_color" + } + ], + "border": { + "corner_radius": 8 + }, + "font_size": 20, + "font_weight": "medium", + "text_alignment_horizontal": "center", + "text_alignment_vertical": "center", + "text_color": "#FDF6E3" + } + }, + "card": { + "log_id": "scoped_actions", + "states": [ + { + "state_id": 0, + "div": { + "type": "container", + "orientation": "vertical", + "paddings": { + "left": 16, + "top": 16, + "right": 16, + "bottom": 16 + }, + "background": [ + { + "type": "solid", + "color": "#EEE8D5" + } + ], + "items": [ + { + "type": "title", + "id": "title", + "text": "@{title_text}", + "margins": { + "bottom": 16 + }, + "variables": [ + { + "name": "title_text", + "type": "string", + "value": "Lorem ipsum" + } + ] + }, + { + "type": "base_text", + "id": "content", + "text": "@{content_text}", + "margins": { + "bottom": 16 + }, + "variables": [ + { + "name": "content_text", + "type": "string", + "value": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." + } + ] + }, + { + "type": "container", + "orientation": "horizontal", + "items": [ + { + "type": "button", + "margins": { + "right": 16 + }, + "background_color": "#2AA198", + "text": "Scoped", + "actions": [ + { + "log_id": "change_title", + "scope_id": "title", + "url": "div-action://set_variable?name=title_text&value=Title%20has%20been%20changed" + } + ] + }, + { + "type": "button", + "background_color": "#B58900", + "text": "Not scoped", + "actions": [ + { + "log_id": "change_title", + "url": "div-action://set_variable?name=title_text&value=Title%20has%20been%20changed" + } + ] + } + ] + } + ] + } + } + ] + } +} diff --git a/test_data/ui_test_data/actions/scoped_actions_ambiguous_scope.json b/test_data/ui_test_data/actions/scoped_actions_ambiguous_scope.json new file mode 100644 index 000000000..07ba27266 --- /dev/null +++ b/test_data/ui_test_data/actions/scoped_actions_ambiguous_scope.json @@ -0,0 +1,114 @@ +{ + "templates": { + "title": { + "type": "text", + "font_size": 24, + "font_weight": "bold", + "text_alignment_horizontal": "center", + "text_alignment_vertical": "center", + "text_color": "#586E75" + }, + "base_text": { + "type": "text", + "font_size": 18, + "font_weight": "medium", + "text_color": "#002B36" + }, + "button": { + "type": "text", + "height": { + "type": "fixed", + "value": 48 + }, + "paddings": { + "left": 8, + "top": 8, + "right": 8, + "bottom": 8 + }, + "background": [ + { + "type": "solid", + "$color": "background_color" + } + ], + "border": { + "corner_radius": 8 + }, + "font_size": 20, + "font_weight": "medium", + "text_alignment_horizontal": "center", + "text_alignment_vertical": "center", + "text_color": "#FDF6E3" + } + }, + "card": { + "log_id": "scoped_actions_ambiguous_scope", + "states": [ + { + "state_id": 0, + "div": { + "type": "container", + "id": "title", + "orientation": "vertical", + "paddings": { + "left": 16, + "top": 16, + "right": 16, + "bottom": 16 + }, + "background": [ + { + "type": "solid", + "color": "#EEE8D5" + } + ], + "items": [ + { + "type": "title", + "id": "title", + "text": "@{title_text}", + "margins": { + "bottom": 16 + }, + "variables": [ + { + "name": "title_text", + "type": "string", + "value": "Lorem ipsum" + } + ] + }, + { + "type": "base_text", + "id": "content", + "text": "@{content_text}", + "margins": { + "bottom": 16 + }, + "variables": [ + { + "name": "content_text", + "type": "string", + "value": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." + } + ] + }, + { + "type": "button", + "background_color": "#B58900", + "text": "Ambiguous scope", + "actions": [ + { + "log_id": "change_some_text", + "scope_id": "title", + "url": "div-action://set_variable?name=title_text&value=Title%20has%20been%20changed" + } + ] + } + ] + } + } + ] + } +} diff --git a/test_data/ui_test_data/actions/scoped_actions_nested_scope.json b/test_data/ui_test_data/actions/scoped_actions_nested_scope.json new file mode 100644 index 000000000..8ffd41c39 --- /dev/null +++ b/test_data/ui_test_data/actions/scoped_actions_nested_scope.json @@ -0,0 +1,132 @@ +{ + "templates": { + "title": { + "type": "text", + "font_size": 24, + "font_weight": "bold", + "text_alignment_horizontal": "center", + "text_alignment_vertical": "center", + "text_color": "#586E75" + }, + "base_text": { + "type": "text", + "font_size": 18, + "font_weight": "medium", + "text_color": "#002B36" + }, + "button": { + "type": "text", + "height": { + "type": "fixed", + "value": 48 + }, + "paddings": { + "left": 8, + "top": 8, + "right": 8, + "bottom": 8 + }, + "background": [ + { + "type": "solid", + "$color": "background_color" + } + ], + "border": { + "corner_radius": 8 + }, + "font_size": 20, + "font_weight": "medium", + "text_alignment_horizontal": "center", + "text_alignment_vertical": "center", + "text_color": "#FDF6E3" + } + }, + "card": { + "log_id": "scoped_actions_nested_scope", + "states": [ + { + "state_id": 0, + "div": { + "type": "container", + "orientation": "vertical", + "paddings": { + "left": 16, + "top": 16, + "right": 16, + "bottom": 16 + }, + "background": [ + { + "type": "solid", + "color": "#EEE8D5" + } + ], + "items": [ + { + "type": "title", + "id": "title", + "text": "@{title_text}", + "margins": { + "bottom": 16 + } + }, + { + "type": "base_text", + "id": "content", + "text": "@{content_text}", + "margins": { + "bottom": 16 + } + }, + { + "type": "container", + "orientation": "horizontal", + "items": [ + { + "type": "button", + "margins": { + "right": 16 + }, + "background_color": "#2AA198", + "text": "Title scope", + "actions": [ + { + "log_id": "change_title", + "scope_id": "title", + "url": "div-action://set_variable?name=title_text&value=Title%20has%20been%20changed%20from%20'title'%20scope" + } + ] + }, + { + "type": "button", + "background_color": "#268BD2", + "text": "Content scope", + "actions": [ + { + "log_id": "change_title", + "scope_id": "content", + "url": "div-action://set_variable?name=title_text&value=Title%20has%20been%20changed%20from%20'content'%20scope" + } + ] + } + ] + } + ], + "variables": [ + { + "name": "title_text", + "type": "string", + "value": "Lorem ipsum" + }, + { + "name": "content_text", + "type": "string", + "value": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." + } + ] + } + } + ] + } +}