Store screenshot generator as UI test

This commit is contained in:
Grishka 2022-04-13 04:39:14 +03:00
parent 0f61cb12d4
commit 3d7f2c154a
7 changed files with 289 additions and 5 deletions

View File

@ -12,6 +12,7 @@ android {
targetSdk 31
versionCode 26
versionName "0.1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
@ -72,4 +73,9 @@ dependencies {
appcenterPrivateBetaImplementation "com.microsoft.appcenter:appcenter-distribute:${appCenterSdkVersion}"
appcenterPublicBetaImplementation "com.microsoft.appcenter:appcenter-crashes:${appCenterSdkVersion}"
appcenterPublicBetaImplementation "com.microsoft.appcenter:appcenter-distribute:${appCenterSdkVersion}"
androidTestImplementation 'androidx.test:core:1.4.1-alpha05'
androidTestImplementation 'androidx.test.ext:junit:1.1.4-alpha05'
androidTestImplementation 'androidx.test:runner:1.5.0-alpha02'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0-alpha05'
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

View File

@ -0,0 +1,252 @@
package org.joinmastodon.android.test;
import android.app.Instrumentation;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import org.hamcrest.CoreMatchers;
import org.hamcrest.Matcher;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.MainActivity;
import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.instance.GetInstance;
import org.joinmastodon.android.api.requests.statuses.GetStatusByID;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.ComposeFragment;
import org.joinmastodon.android.fragments.ThreadFragment;
import org.joinmastodon.android.fragments.onboarding.InstanceRulesFragment;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.junit.Assert;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.parceler.Parcels;
import java.io.File;
import java.io.IOException;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.TimeoutException;
import androidx.test.core.app.ActivityScenario;
import androidx.test.espresso.PerformException;
import androidx.test.espresso.UiController;
import androidx.test.espresso.ViewAction;
import androidx.test.espresso.util.HumanReadables;
import androidx.test.espresso.util.TreeIterables;
import androidx.test.ext.junit.rules.ActivityScenarioRule;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.runner.screenshot.ScreenCapture;
import androidx.test.runner.screenshot.Screenshot;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import okio.BufferedSink;
import okio.Okio;
import okio.Sink;
import okio.Source;
import static androidx.test.espresso.Espresso.*;
import static androidx.test.espresso.action.ViewActions.*;
import static androidx.test.espresso.assertion.ViewAssertions.*;
import static androidx.test.espresso.matcher.ViewMatchers.*;
@RunWith(AndroidJUnit4.class)
@LargeTest
public class StoreScreenshotsGenerator{
private static final String PHOTO_FILE="IMG_1010.jpg";
@Rule
public ActivityScenarioRule<MainActivity> activityScenarioRule=new ActivityScenarioRule<>(MainActivity.class);
@Test
public void takeScreenshots() throws Exception{
File photo=new File(MastodonApp.context.getCacheDir(), PHOTO_FILE);
try(Source source=Okio.source(InstrumentationRegistry.getInstrumentation().getContext().getAssets().open(PHOTO_FILE)); BufferedSink sink=Okio.buffer(Okio.sink(photo))){
sink.writeAll(source);
sink.flush();
}
GlobalUserPreferences.theme=GlobalUserPreferences.ThemePreference.LIGHT;
Bundle args=InstrumentationRegistry.getArguments();
InstrumentationRegistry.getInstrumentation().setInTouchMode(true);
AccountSession session=AccountSessionManager.getInstance().getAccount(AccountSessionManager.getInstance().getLastActiveAccountID());
MastodonApp.context.deleteDatabase(session.getID()+".db");
onView(isRoot()).perform(waitId(R.id.more, 5000));
Thread.sleep(500);
takeScreenshot("HomeTimeline");
GlobalUserPreferences.theme=GlobalUserPreferences.ThemePreference.DARK;
activityScenarioRule.getScenario().recreate();
onView(isRoot()).perform(waitId(R.id.more, 5000));
Thread.sleep(500);
takeScreenshot("HomeTimeline_Dark");
GlobalUserPreferences.theme=GlobalUserPreferences.ThemePreference.LIGHT;
activityScenarioRule.getScenario().recreate();
activityScenarioRule.getScenario().onActivity(activity->UiUtils.openProfileByID(activity, session.getID(), args.getString("profileAccountID")));
Thread.sleep(500);
onView(isRoot()).perform(waitId(R.id.avatar_border, 5000)); // wait for profile to load
onView(isRoot()).perform(waitId(R.id.more, 5000)); // wait for timeline to load
Thread.sleep(500);
takeScreenshot("Profile");
Status[] _status={null};
CyclicBarrier barrier=new CyclicBarrier(2);
new GetStatusByID(args.getString("threadPostID"))
.setCallback(new Callback<>(){
@Override
public void onSuccess(Status result){
_status[0]=result;
try{
barrier.await();
}catch(Exception ignore){}
}
@Override
public void onError(ErrorResponse error){
try{
barrier.await();
}catch(Exception ignore){}
}
})
.exec(session.getID());
barrier.await();
Assert.assertNotNull(_status[0]);
ThreadFragment[] _fragment={null};
activityScenarioRule.getScenario().onActivity(activity->{
activity.getSystemService(InputMethodManager.class).hideSoftInputFromWindow(activity.getWindow().getDecorView().getWindowToken(), 0);
Bundle threadArgs=new Bundle();
threadArgs.putParcelable("status", Parcels.wrap(_status[0]));
threadArgs.putString("account", session.getID());
threadArgs.putBoolean("_can_go_back", true);
ThreadFragment fragment=new ThreadFragment();
fragment.setArguments(threadArgs);
activity.showFragment(fragment);
_fragment[0]=fragment;
});
while(!_fragment[0].loaded){
Thread.sleep(50);
}
Thread.sleep(300);
takeScreenshot("Thread");
Instance[] _instance={null};
new GetInstance()
.setCallback(new Callback<>(){
@Override
public void onSuccess(Instance result){
_instance[0]=result;
try{
barrier.await();
}catch(Exception ignore){}
}
@Override
public void onError(ErrorResponse error){
try{
barrier.await();
}catch(Exception ignore){}
}
})
.execNoAuth("mastodon.social");
barrier.await();
Assert.assertNotNull(_instance[0]);
activityScenarioRule.getScenario().onActivity(activity->{
Bundle rulesArgs=new Bundle();
rulesArgs.putParcelable("instance", Parcels.wrap(_instance[0]));
InstanceRulesFragment fragment=new InstanceRulesFragment();
fragment.setArguments(rulesArgs);
activity.showFragment(fragment);
});
Thread.sleep(500);
takeScreenshot("InstanceRules");
activityScenarioRule.getScenario().onActivity(activity->{
activity.onBackPressed();
Bundle composeArgs=new Bundle();
composeArgs.putString("account", session.getID());
ComposeFragment fragment=new ComposeFragment();
fragment.setArguments(composeArgs);
activity.showFragment(fragment);
fragment.addFakeMediaAttachment(Uri.fromFile(photo), "Pantheon");
});
onView(withId(R.id.toot_text)).perform(typeText("This is a picture I took the last time I visited #Athens, Greece. What a beautiful place!"));
InstrumentationRegistry.getInstrumentation().setInTouchMode(true);
takeScreenshot("Compose");
GlobalUserPreferences.theme=GlobalUserPreferences.ThemePreference.DARK;
activityScenarioRule.getScenario().recreate();
Thread.sleep(500);
takeScreenshot("Compose_Dark");
GlobalUserPreferences.theme=GlobalUserPreferences.ThemePreference.LIGHT;
activityScenarioRule.getScenario().recreate();
}
private void takeScreenshot(String name) throws IOException{
Screenshot.capture().setName(name).setFormat(Bitmap.CompressFormat.PNG).process();
}
/**
* Perform action of waiting for a specific view id.
* @param viewId The id of the view to wait for.
* @param millis The timeout of until when to wait for.
*/
public static ViewAction waitId(final int viewId, final long millis) {
return new ViewAction() {
@Override
public Matcher<View> getConstraints() {
return isRoot();
}
@Override
public String getDescription() {
return "wait for a specific view with id <" + viewId + "> during " + millis + " millis.";
}
@Override
public void perform(final UiController uiController, final View view) {
uiController.loopMainThreadUntilIdle();
final long startTime = System.currentTimeMillis();
final long endTime = startTime + millis;
final Matcher<View> viewMatcher=CoreMatchers.allOf(withId(viewId), withEffectiveVisibility(Visibility.VISIBLE));
do {
for (View child : TreeIterables.breadthFirstViewTraversal(view)) {
// found view with required ID
if (viewMatcher.matches(child)) {
return;
}
}
uiController.loopMainThreadForAtLeast(50);
}
while (System.currentTimeMillis() < endTime);
// timeout happens
throw new PerformException.Builder()
.withActionDescription(this.getDescription())
.withViewDescription(HumanReadables.describe(view))
.withCause(new TimeoutException())
.build();
}
};
}
}

View File

@ -186,6 +186,9 @@ public class MastodonAPIController{
req.onError("Error parsing an API error", response.code());
}
}
}catch(Exception x){
Log.w(TAG, "onResponse: error processing response", x);
onFailure(call, (IOException) new IOException(x).fillInStackTrace());
}
}
});

View File

@ -0,0 +1,10 @@
package org.joinmastodon.android.api.requests.statuses;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Status;
public class GetStatusByID extends MastodonAPIRequest<Status>{
public GetStatusByID(String id){
super(HttpMethod.GET, "/statuses/"+id, Status.class);
}
}

View File

@ -91,6 +91,7 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import androidx.annotation.DrawableRes;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
@ -796,6 +797,16 @@ public class ComposeFragment extends ToolbarFragment implements OnBackPressedLis
return thumb;
}
public void addFakeMediaAttachment(Uri uri, String description){
pollBtn.setEnabled(false);
DraftMediaAttachment draft=new DraftMediaAttachment();
draft.uri=uri;
draft.description=description;
attachmentsView.addView(createMediaAttachmentView(draft));
allAttachments.add(draft);
attachmentsView.setVisibility(View.VISIBLE);
}
private void uploadMediaAttachment(DraftMediaAttachment attachment){
if(uploadingAttachment!=null)
throw new IllegalStateException("there is already an attachment being uploaded");

View File

@ -172,12 +172,14 @@ public class UiUtils{
}
public static String getFileName(Uri uri){
if(uri.getScheme().equals("content")){
try(Cursor cursor=MastodonApp.context.getContentResolver().query(uri, new String[]{OpenableColumns.DISPLAY_NAME}, null, null, null)){
cursor.moveToFirst();
String name=cursor.getString(0);
if(name!=null)
return name;
}
}
return uri.getLastPathSegment();
}