/** * Copyright (c) 2017-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. */ package com.facebook.litho.testing; import java.util.ArrayList; import java.util.List; import android.graphics.Rect; import android.view.View; import com.facebook.litho.Component; import com.facebook.litho.ComponentContext; import com.facebook.litho.ComponentLifecycle; import com.facebook.litho.ComponentTree; import com.facebook.litho.LithoView; import com.facebook.litho.ComponentsPools; import com.facebook.litho.EventHandler; import com.facebook.litho.TestComponentTree; import com.facebook.litho.TreeProps; import org.powermock.reflect.Whitebox; import static android.view.View.MeasureSpec.EXACTLY; import static android.view.View.MeasureSpec.UNSPECIFIED; import static android.view.View.MeasureSpec.makeMeasureSpec; /** * Helper class to simplify testing of components. * * Allows simple and short creation of views that are created and mounted in a similar way to how * they are in real apps. */ public final class ComponentTestHelper { /** * Mount a component into a component view. * * @param component The component builder to mount * @return A LithoView with the component mounted in it. */ public static LithoView mountComponent(Component.Builder component) { return mountComponent(getContext(component), component.build()); } /** * Mount a component into a component view. * * @param component The component builder to mount * @param incrementalMountEnabled States whether incremental mount is enabled * @return A LithoView with the component mounted in it. */ public static LithoView mountComponent( Component.Builder component, boolean incrementalMountEnabled) { ComponentContext context = getContext(component); return mountComponent( context, new LithoView(context), component.build(), incrementalMountEnabled, 100, 100); } /** * Mount a component into a component view. * * @param context A components context * @param component The component to mount * @return A LithoView with the component mounted in it. */ public static LithoView mountComponent(ComponentContext context, Component component) { return mountComponent(context, component, 100, 100); } /** * Mount a component into a component view. * * @param context A components context * @param component The component to mount * @param width The width of the resulting view * @param height The height of the resulting view * @return A LithoView with the component mounted in it. */ public static LithoView mountComponent( ComponentContext context, Component component, int width, int height) { return mountComponent(context, new LithoView(context), component, width, height); } /** * Mount a component into a component view. * * @param context A components context * @param lithoView The view to mount the component into * @param component The component to mount * @return A LithoView with the component mounted in it. */ public static LithoView mountComponent( ComponentContext context, LithoView lithoView, Component component) { return mountComponent(context, lithoView, component, 100, 100); } /** * Mount a component into a component view. * * @param context A components context * @param lithoView The view to mount the component into * @param component The component to mount * @param width The width of the resulting view * @param height The height of the resulting view * @return A LithoView with the component mounted in it. */ public static LithoView mountComponent( ComponentContext context, LithoView lithoView, Component component, int width, int height) { return mountComponent( context, lithoView, component, false, width, height); } /** * Mount a component into a component view. * * @param context A components context * @param lithoView The view to mount the component into * @param component The component to mount * @param incrementalMountEnabled States whether incremental mount is enabled * @param width The width of the resulting view * @param height The height of the resulting view * @return A LithoView with the component mounted in it. */ public static LithoView mountComponent( ComponentContext context, LithoView lithoView, Component component, boolean incrementalMountEnabled, int width, int height) { return mountComponent( lithoView, ComponentTree.create(context, component) .incrementalMount(incrementalMountEnabled) .layoutDiffing(false) .build(), makeMeasureSpec(width, EXACTLY), makeMeasureSpec(height, EXACTLY)); } /** * Mount a component tree into a component view. * * @param lithoView The view to mount the component tree into * @param componentTree The component tree to mount * @return A LithoView with the component tree mounted in it. */ public static LithoView mountComponent( LithoView lithoView, ComponentTree componentTree) { return mountComponent( lithoView, componentTree, makeMeasureSpec(100, EXACTLY), makeMeasureSpec(100, EXACTLY)); } /** * Mount a component tree into a component view. * * @param lithoView The view to mount the component tree into * @param componentTree The component tree to mount * @param widthSpec The width spec used to measure the resulting view * @param heightSpec The height spec used to measure the resulting view * @return A LithoView with the component tree mounted in it. */ public static LithoView mountComponent( LithoView lithoView, ComponentTree componentTree, int widthSpec, int heightSpec) { lithoView.setComponentTree(componentTree); try { Whitebox.invokeMethod(lithoView, "onAttach"); } catch (Exception e) { throw new RuntimeException(e); } lithoView.measure(widthSpec, heightSpec); lithoView.layout( 0, 0, lithoView.getMeasuredWidth(), lithoView.getMeasuredHeight()); return lithoView; } /** * Unmounts a component tree from a component view. * @param lithoView the view to unmount */ public static void unmountComponent(LithoView lithoView) { if (!lithoView.isIncrementalMountEnabled()) { throw new IllegalArgumentException( "In order to test unmounting a Component, it needs to be mounted with " + "incremental mount enabled. Please use a mountComponent() variation that " + "accepts an incrementalMountEnabled argument"); } // Unmounting the component by running incremental mount to a Rect that we certain won't // contain the component. Rect rect = new Rect(99999, 99999, 999999, 999999); lithoView.performIncrementalMount(rect); } /** * Unbinds a component tree from a component view. * * @param lithoView The view to unbind. */ public static void unbindComponent(LithoView lithoView) { try { Whitebox.invokeMethod(lithoView, "onDetach"); } catch (Exception e) { throw new RuntimeException(e); } } /** * Get the subcomponents of a component * * @param component The component builder which to get the subcomponents of * @return The subcomponents of the given component */ public static List<SubComponent> getSubComponents(Component.Builder component) { return getSubComponents(getContext(component), component.build()); } /** * Get the subcomponents of a component * * @param context A components context * @param component The component which to get the subcomponents of * @return The subcomponents of the given component */ public static List<SubComponent> getSubComponents(ComponentContext context, Component component) { return getSubComponents( context, component, makeMeasureSpec(1000, EXACTLY), makeMeasureSpec(0, UNSPECIFIED)); } /** * Get the subcomponents of a component * * @param component The component which to get the subcomponents of * @param widthSpec The width to measure the component with * @param heightSpec The height to measure the component with * @return The subcomponents of the given component */ public static List<SubComponent> getSubComponents( Component.Builder component, int widthSpec, int heightSpec) { return getSubComponents(getContext(component), component.build(), widthSpec, heightSpec); } /** * Get the subcomponents of a component * * @param context A components context * @param component The component which to get the subcomponents of * @param widthSpec The width to measure the component with * @param heightSpec The height to measure the component with * @return The subcomponents of the given component */ public static List<SubComponent> getSubComponents( ComponentContext context, Component component, int widthSpec, int heightSpec) { final TestComponentTree componentTree = TestComponentTree.create(context, component) .incrementalMount(false) .build(); final LithoView lithoView = new LithoView(context); lithoView.setComponentTree(componentTree); lithoView.measure(widthSpec, heightSpec); lithoView.layout(0, 0, lithoView.getMeasuredWidth(), lithoView.getMeasuredHeight()); final List<Component> components = componentTree.getSubComponents(); final List<SubComponent> subComponents = new ArrayList<>(components.size()); for (Component lifecycle : components) { subComponents.add(SubComponent.of(lifecycle)); } return subComponents; } /** * Returns the first subComponent of type class. * * @param component The component builder which to get the subcomponent from * @param componentClass the class type of the requested sub component * @return The first instance of subComponent of type Class or null if none is present. */ public static <T extends ComponentLifecycle> Component<T> getSubComponent( Component.Builder component, Class<T> componentClass) { List<SubComponent> subComponents = getSubComponents(component); for (SubComponent subComponent : subComponents) { if (subComponent.getComponentType().equals(componentClass)) { return (Component<T>) subComponent.getComponent(); } } return null; } /** * Measure and layout a component view. * * @param view The component view to measure and layout */ public static void measureAndLayout(View view) { view.measure(makeMeasureSpec(1000, EXACTLY), makeMeasureSpec(0, UNSPECIFIED)); view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight()); } private static ComponentContext getContext(Component.Builder builder) { return Whitebox.getInternalState(builder, "mContext"); } /** * Mounts the component & triggers the visibility event. Requires that the component supports * incremental mounting. * * {@link com.facebook.litho.VisibleEvent} * * @param context A components context * @param onVisibleHandler SpecificComponent.onVisible(component) * @param component The component builder which to get the subcomponent from * @return A LithoView with the component mounted in it. */ public static LithoView dispatchVisibleEvent( ComponentContext context, EventHandler onVisibleHandler, Component component) { LithoView lithoView = new LithoView(context); mountComponent( lithoView, ComponentTree.create(context, component) .layoutDiffing(false) .build(), makeMeasureSpec(100, EXACTLY), makeMeasureSpec(100, EXACTLY)); lithoView.performIncrementalMount(); try { Whitebox.invokeMethod(component.getLifecycle(), "dispatchOnVisible", onVisibleHandler); } catch (Exception e) { throw new RuntimeException(e); } return lithoView; } /** * Sets a TreeProp that will be visible to all Components which are created from * the given Context (unless a child overwrites its). */ public static void setTreeProp(ComponentContext context, Class propClass, Object prop) { TreeProps treeProps; try { treeProps = Whitebox.invokeMethod(context, "getTreeProps"); if (treeProps == null) { treeProps = ComponentsPools.acquireTreeProps(); Whitebox.invokeMethod(context, "setTreeProps", treeProps); } } catch (Exception e) { throw new RuntimeException(e); } treeProps.put(propClass, prop); } }