使用Espresso等待ViewPager动画?

17

尝试使用ViewPager进行一些测试。

我想在选项卡之间滑动,且必须等到滑动完成后再进行下一步操作。但是似乎没有办法关闭ViewPager的动画(开发人员选项中的所有动画都已禁用)。

因此,这总会导致测试失败,因为ViewPager尚未完成动画,因此视图尚未完全显示:

// swipe left
onView(withId(R.id.viewpager)).check(matches(isDisplayed())).perform(swipeLeft());

// check to ensure that the next tab is completely visible.
onView(withId(R.id.next_tab)).check(matches(isCompletelyDisplayed()));

有没有一种优雅甚至是推荐的方法来做到这一点,或者我必须在那里放置某种定时等待?

7个回答

29

IdlingResource是Simas建议的,实际上非常容易实现:

public class ViewPagerIdlingResource implements IdlingResource {

    private final String mName;

    private boolean mIdle = true; // Default to idle since we can't query the scroll state.

    private ResourceCallback mResourceCallback;

    public ViewPagerIdlingResource(ViewPager viewPager, String name) {
        viewPager.addOnPageChangeListener(new ViewPagerListener());
        mName = name;
    }

    @Override
    public String getName() {
        return mName;
    }

    @Override
    public boolean isIdleNow() {
        return mIdle;
    }

    @Override
    public void registerIdleTransitionCallback(ResourceCallback resourceCallback) {
        mResourceCallback = resourceCallback;
    }

    private class ViewPagerListener extends ViewPager.SimpleOnPageChangeListener {

        @Override
        public void onPageScrollStateChanged(int state) {
            mIdle = (state == ViewPager.SCROLL_STATE_IDLE
                    // Treat dragging as idle, or Espresso will block itself when swiping.
                    || state == ViewPager.SCROLL_STATE_DRAGGING);
            if (mIdle && mResourceCallback != null) {
                mResourceCallback.onTransitionToIdle();
            }
        }
    }
}

2
我想,我只需要在每个测试中注册一次ViewPagerIdlingResource,这样我就可以在测试开始时注册VPIR,然后执行onView().perform(swipeLeft),再执行onView().perform(swipeLeft),最后检查我的视图。我不需要在两次向右滑动之间取消注册和注册我的VPIR,对吗? - flobacca
这对我没有用,我需要将ViewPager.SCROLL_STATE_DRAGGING状态视为非空闲状态,但如果我删除它,Espresso会像代码中的注释所说的那样阻塞自己。有任何想法为什么会发生这种情况? - user2880229
为什么你需要将拖动状态视为非空闲状态? - vaughandroid
当我运行测试时,似乎Espresso没有等待滑动完成,因此它尝试在视图完全可见之前匹配视图。使用这种方法,测试有时通过,有时不通过。我提供了一个稍微不同的实现,目前看起来可以工作。 - user2880229

4

1
感谢这次迁移!ViewPager2Actions 工作得非常好,让我的测试运行更快! - LeMimit

3

由于我至少已经做过这两次,因此这里是在 Kotlin 和使用 androidx 的 ViewPager2 中的被接受的答案

class ViewPager2IdlingResource(viewPager: ViewPager2, name: String) : IdlingResource {

    private val name: String
    private var isIdle = true // Default to idle since we can't query the scroll state.
    private var resourceCallback: IdlingResource.ResourceCallback? = null

    init {
        viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
            override fun onPageScrollStateChanged(state: Int) {
                isIdle = (state == ViewPager.SCROLL_STATE_IDLE // Treat dragging as idle, or Espresso will block itself when swiping.
                    || state == ViewPager.SCROLL_STATE_DRAGGING)
                if (isIdle && resourceCallback != null) {
                    resourceCallback!!.onTransitionToIdle()
                }
            }
        })
        this.name = name
    }

    override fun getName(): String {
        return name
    }

    override fun isIdleNow(): Boolean {
        return isIdle
    }

    override fun registerIdleTransitionCallback(resourceCallback: IdlingResource.ResourceCallback) {
        this.resourceCallback = resourceCallback
    }
}

接下来是使用 ActivityScenarioRule 进行 UI 测试的方法:

@get:Rule
val testRule = ActivityScenarioRule(OnboardingActivity::class.java)

private lateinit var viewPager2IdlingResource: ViewPager2IdlingResource

....

@Before
fun setUp() {
    testRule.scenario.onActivity {
        viewPager2IdlingResource =
            ViewPager2IdlingResource(it.findViewById(R.id.onboarding_view_pager), "viewPagerIdlingResource")
        IdlingRegistry.getInstance().register(viewPager2IdlingResource)
    }
}

@After
fun tearDown() {
    IdlingRegistry.getInstance().unregister(viewPager2IdlingResource)
}

0

试试这个,

    onView(withId(R.id.pager)).perform(pagerSwipeRight()).perform(pagerSwipeLeft());

    private GeneralSwipeAction pagerSwipeRight(){
        return new GeneralSwipeAction(Swipe.SLOW, GeneralLocation.CENTER_LEFT,
                GeneralLocation.CENTER_RIGHT, Press.FINGER);
    }

    private GeneralSwipeAction pagerSwipeLeft(){
        return new GeneralSwipeAction(Swipe.SLOW, GeneralLocation.CENTER_RIGHT,
                GeneralLocation.CENTER_LEFT, Press.FINGER);
    }

仅仅通过引入自定义滑动并将 Swipe.FAST 替换为 Swipe.SLOW 可能无法解决核心问题,我已经尝试过了!更好的方法是按照上面的答案建议创建一个自定义的 IdlingResource - Wahib Ul Haq

0

我在使用@vaughandroid的方法时遇到了问题,所以我对他的方法进行了一些更改。这种方法会在检测到滚动发生时将空闲设置为false,并通过使用setCurrentItem()“强制”ViewPager完成滚动。

public class ViewPagerIdlingResource implements IdlingResource {
    private volatile boolean mIdle = true; // Default to idle since we can't query the scroll state.

    private ResourceCallback mResourceCallback;

    private ViewPager mViewPager;

    public static ViewPagerIdlingResource waitViewPagerSwipe(ViewPager viewPager) {
        return new ViewPagerIdlingResource(viewPager);
    }

    private ViewPagerIdlingResource(ViewPager viewPager) {
        mViewPager = viewPager;
        mViewPager.addOnPageChangeListener(new ViewPagerListener());
    }

    @Override
    public String getName() {
        return ViewPagerIdlingResource.class.getSimpleName();
    }

    @Override
    public boolean isIdleNow() {
        return mIdle;
    }

    @Override
    public void registerIdleTransitionCallback(ResourceCallback resourceCallback) {
        mResourceCallback = resourceCallback;
    }

    private class ViewPagerListener extends ViewPager.SimpleOnPageChangeListener {
        float mPositionOffset = 0.0f;

        @Override
        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
            if (isSwipingToRight(positionOffset)) {
                mIdle = false;
                mViewPager.setCurrentItem(position + 1);
            } else if (isSwipingToLeft(positionOffset)) {
                mIdle = false;
                mViewPager.setCurrentItem(position - 1);
            }

            mPositionOffset = positionOffset;

            if (positionOffset == 0 && !mIdle && mResourceCallback != null) {
                mResourceCallback.onTransitionToIdle();
                mIdle = true;
                mPositionOffset = 0.0f;
            }
        }

        private boolean isSwipingToRight(float positionOffset) {
            return mPositionOffset != 0.0f && positionOffset > mPositionOffset && mIdle;
        }

        private boolean isSwipingToLeft(float positionOffset) {
            return mPositionOffset != 0.0f && positionOffset < mPositionOffset && mIdle;
        }
    }
}

0
我的目标是使用Facebook截图测试库对屏幕上的ViewPager2进行截图。对我来说最简单的方法是几乎检查每一帧是否完成动画,如果是,则是时候进行截图了。
fun waitForViewPagerAnimation(parentView: View) {
    if (parentView is ViewGroup) {
        parentView.childrenViews<ViewPager2>().forEach {
            while (it.scrollState != ViewPager2.SCROLL_STATE_IDLE) {
                Thread.sleep(16)
            }
        }
    }
}

childrenViews 函数可以在这里找到


-10

我真的很想避免sleep这个操作。 - Mark
3
睡眠动画时间是一种非常难以维护的做法。而且它会使测试代码变得混乱,因为它很快就会包含 50% 的睡眠语句。您应该真正采用空闲资源解决方案,这将从测试代码逻辑中分离出空闲等待。 - david.schreiber
这也可能导致不一致、不可靠的测试,因为不同的系统或测试迭代需要更多/更少的等待时间。它还可能逐渐增加测试套件的显著时间成本。 - Andrea Thacker

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接