package org.wikipedia.test.view; import android.content.Context; import android.content.res.Configuration; import android.net.Uri; import android.os.Build; import android.support.annotation.DrawableRes; import android.support.annotation.IdRes; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.StringRes; import android.support.v4.text.TextUtilsCompat; import android.view.ContextThemeWrapper; import android.view.View; import android.widget.TextView; import com.facebook.common.util.UriUtil; import com.facebook.testing.screenshot.Screenshot; import com.facebook.testing.screenshot.ViewHelpers; import org.apache.commons.lang3.ArrayUtils; import org.junit.experimental.theories.DataPoints; import org.junit.experimental.theories.Theories; import org.junit.runner.RunWith; import org.wikipedia.theme.Theme; import org.wikipedia.util.ResourceUtil; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Locale; import static android.support.test.InstrumentationRegistry.getTargetContext; import static butterknife.ButterKnife.findById; import static org.apache.commons.lang3.StringUtils.defaultString; import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertThat; import static org.wikipedia.test.TestUtil.runOnMainSync; @RunWith(Theories.class) public abstract class ViewTest { @SuppressWarnings("WeakerAccess") @DataPoints public static final Locale[] LOCALES = {Locale.ENGLISH}; @SuppressWarnings("unused") @DataPoints public static final LayoutDirection[] LAYOUT_DIRECTIONS = LayoutDirection.values(); @SuppressWarnings("unused") @DataPoints public static final FontScale[] FONT_SCALES = FontScale.values(); @SuppressWarnings("unused") @DataPoints public static final Theme[] THEMES = Theme.values(); @SuppressWarnings("unused") @DataPoints public static final PrimaryTestStr[] PRIMARY_STRS = PrimaryTestStr.values(); @SuppressWarnings("unused") @DataPoints public static final SecondaryTestStr[] SECONDARY_STRS = SecondaryTestStr.values(); @SuppressWarnings("unused") @DataPoints public static final PrimaryTestImg[] PRIMARY_IMGS = PrimaryTestImg.values(); @SuppressWarnings("unused") @DataPoints public static final SecondaryTestImg[] SECONDARY_IMGS = SecondaryTestImg.values(); protected static final int WIDTH_DP_XL = 720; protected static final int WIDTH_DP_L = 480; protected static final int WIDTH_DP_M = 320; protected static final int WIDTH_DP_S = 240; protected static final int WIDTH_DP_XS = 120; protected static final int HEIGHT_DP_L = 720; protected static final int HEIGHT_DP_M = 480; protected static final int HEIGHT_DP_S = 320; protected static final int HEIGHT_DP_XS = 240; private int widthDp; @Nullable private Integer heightDp; private Locale locale; private LayoutDirection layoutDirection; private FontScale fontScale; private Theme theme; private Context ctx; protected void setUp(int widthDp, @NonNull LayoutDirection layoutDirection, @NonNull FontScale fontScale, @NonNull Theme theme) { setUp(widthDp, null, LOCALES[0], layoutDirection, fontScale, theme); } protected void setUp(int widthDp, int heightDp, @NonNull LayoutDirection layoutDirection, @NonNull FontScale fontScale, @NonNull Theme theme) { setUp(widthDp, heightDp, LOCALES[0], layoutDirection, fontScale, theme); } protected void setUp(int widthDp, @Nullable Integer heightDp, @NonNull Locale locale, @NonNull LayoutDirection layoutDirection, @NonNull FontScale fontScale, @NonNull Theme theme) { this.widthDp = widthDp; this.heightDp = heightDp; this.locale = locale; this.layoutDirection = layoutDirection; this.fontScale = fontScale; this.theme = theme; ctx = new ContextThemeWrapper(getTargetContext(), theme.getResourceId()); config(); } protected void snap(@NonNull View subject, @Nullable String... dataPoints) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { int rtl = layoutDirection == LayoutDirection.RTL ? View.LAYOUT_DIRECTION_RTL : TextUtilsCompat.getLayoutDirectionFromLocale(locale); //noinspection WrongConstant subject.setLayoutDirection(rtl); } ViewHelpers viewHelpers = ViewHelpers.setupView(subject).setExactWidthDp(widthDp); if (heightDp != null) { viewHelpers.setExactHeightDp(heightDp); } viewHelpers.layout(); List<String> list = new ArrayList<>(); String byHeight = heightDp == null ? "" : ("x" + heightDp); list.add(widthDp + byHeight + "dp"); list.add(locale.toString()); list.add(layoutDirection == LayoutDirection.RTL ? "rtl" : "ltr"); list.add("font" + fontScale.multiplier() + "x"); list.add(theme.toString().toLowerCase()); list.addAll(Arrays.asList(ArrayUtils.nullToEmpty(dataPoints))); Screenshot.snap(subject).setName(testName(list)).record(); } protected void requestFocus(@NonNull final View view) { runOnMainSync(new Runnable() { @Override public void run() { view.requestFocus(); } }); } protected void assertText(@NonNull View subject, @IdRes int id, @NonNull TestStr text) { assertText(subject, id, text.id()); } protected void assertText(@NonNull View subject, @IdRes int id, @StringRes int text) { TextView textView = findById(subject, id); assertThat(textView.getText().toString(), is(defaultString(str(text)))); } @Nullable protected Uri frescoUri(@DrawableRes int id) { return id == 0 ? null : new Uri.Builder() .scheme(UriUtil.LOCAL_RESOURCE_SCHEME) .path(String.valueOf(id)) .build(); } protected String str(@NonNull TestStr str, @Nullable Object... formatArgs) { return str(str.id(), formatArgs); } protected String str(@StringRes int id, @Nullable Object... formatArgs) { return id == 0 ? null : ctx().getString(id, formatArgs); } protected Context ctx() { return ctx; } private void config() { Configuration cfg = new Configuration(ctx.getResources().getConfiguration()); cfg.screenWidthDp = widthDp; cfg.fontScale = fontScale.multiplier(); ResourceUtil.setLocale(ctx, cfg, locale); } // todo: identify method name by @Theory / @Test annotation instead of depth and remove repeated // calls to snap() private String testName(@NonNull Iterable<String> dataPoints) { final int depth = 4; StackTraceElement element = Thread.currentThread().getStackTrace()[depth]; String name = element.getClassName() + '.' + element.getMethodName(); for (String dataPoint : dataPoints) { name += '-' + dataPoint; } return name; } }