Store screenshot generator as UI test
This commit is contained in:
parent
0f61cb12d4
commit
3d7f2c154a
|
@ -12,6 +12,7 @@ android {
|
||||||
targetSdk 31
|
targetSdk 31
|
||||||
versionCode 26
|
versionCode 26
|
||||||
versionName "0.1"
|
versionName "0.1"
|
||||||
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
|
@ -72,4 +73,9 @@ dependencies {
|
||||||
appcenterPrivateBetaImplementation "com.microsoft.appcenter:appcenter-distribute:${appCenterSdkVersion}"
|
appcenterPrivateBetaImplementation "com.microsoft.appcenter:appcenter-distribute:${appCenterSdkVersion}"
|
||||||
appcenterPublicBetaImplementation "com.microsoft.appcenter:appcenter-crashes:${appCenterSdkVersion}"
|
appcenterPublicBetaImplementation "com.microsoft.appcenter:appcenter-crashes:${appCenterSdkVersion}"
|
||||||
appcenterPublicBetaImplementation "com.microsoft.appcenter:appcenter-distribute:${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 |
|
@ -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();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -186,6 +186,9 @@ public class MastodonAPIController{
|
||||||
req.onError("Error parsing an API error", response.code());
|
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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -91,6 +91,7 @@ import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import androidx.annotation.DrawableRes;
|
||||||
import me.grishka.appkit.Nav;
|
import me.grishka.appkit.Nav;
|
||||||
import me.grishka.appkit.api.Callback;
|
import me.grishka.appkit.api.Callback;
|
||||||
import me.grishka.appkit.api.ErrorResponse;
|
import me.grishka.appkit.api.ErrorResponse;
|
||||||
|
@ -796,6 +797,16 @@ public class ComposeFragment extends ToolbarFragment implements OnBackPressedLis
|
||||||
return thumb;
|
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){
|
private void uploadMediaAttachment(DraftMediaAttachment attachment){
|
||||||
if(uploadingAttachment!=null)
|
if(uploadingAttachment!=null)
|
||||||
throw new IllegalStateException("there is already an attachment being uploaded");
|
throw new IllegalStateException("there is already an attachment being uploaded");
|
||||||
|
|
|
@ -172,12 +172,14 @@ public class UiUtils{
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getFileName(Uri uri){
|
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)){
|
try(Cursor cursor=MastodonApp.context.getContentResolver().query(uri, new String[]{OpenableColumns.DISPLAY_NAME}, null, null, null)){
|
||||||
cursor.moveToFirst();
|
cursor.moveToFirst();
|
||||||
String name=cursor.getString(0);
|
String name=cursor.getString(0);
|
||||||
if(name!=null)
|
if(name!=null)
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return uri.getLastPathSegment();
|
return uri.getLastPathSegment();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue