diff --git a/detox/android/detox/src/main/java/com/wix/detox/DetoxManager.java b/detox/android/detox/src/main/java/com/wix/detox/DetoxManager.java index 0cbb2e858d..c11d7bd061 100644 --- a/detox/android/detox/src/main/java/com/wix/detox/DetoxManager.java +++ b/detox/android/detox/src/main/java/com/wix/detox/DetoxManager.java @@ -11,6 +11,9 @@ import com.wix.detox.systeminfo.Environment; import com.wix.invoke.MethodInvocation; +import org.json.JSONException; +import org.json.JSONObject; + import java.lang.reflect.InvocationTargetException; import java.util.Collections; import java.util.HashMap; @@ -119,7 +122,16 @@ public void run() { break; case "cleanup": wsClient.sendAction("cleanupDone", Collections.emptyMap(), messageId); - stop(); + try { + boolean stopRunner = new JSONObject(params).getBoolean("stopRunner"); + if (stopRunner) { + stop(); + } else { + ReactNativeSupport.removeEspressoIdlingResources(reactNativeHostHolder); + } + } catch (JSONException e) { + Log.e(LOG_TAG, "cleanup cmd doesn't have stopRunner param"); + } break; case "reactNativeReload": UiAutomatorHelper.espressoSync(); @@ -138,6 +150,6 @@ public void onConnect() { @Override public void onClosed() { -// stop(); + stop(); } } diff --git a/detox/android/detox/src/main/java/com/wix/detox/ReactNativeSupport.java b/detox/android/detox/src/main/java/com/wix/detox/ReactNativeSupport.java index ba9dd9167e..dac86fd96e 100644 --- a/detox/android/detox/src/main/java/com/wix/detox/ReactNativeSupport.java +++ b/detox/android/detox/src/main/java/com/wix/detox/ReactNativeSupport.java @@ -274,7 +274,7 @@ private static void setupEspressoIdlingResources( rnUIModuleIdlingResource); } - private static ArrayList looperIdlingResources = new ArrayList<>(); + private static ArrayList looperIdlingResources = new ArrayList<>(); private static void setupReactNativeQueueInterrogators(@NonNull Object reactContext) { HashSet excludedLoopers = new HashSet<>(); @@ -364,7 +364,8 @@ private static void removeEspressoIdlingResources( } private static void removeReactNativeQueueInterrogators() { - for (IdlingResource res : looperIdlingResources) { + for (LooperIdlingResource res : looperIdlingResources) { + res.stop(); Espresso.unregisterIdlingResources(res); } looperIdlingResources.clear(); diff --git a/detox/android/detox/src/main/java/com/wix/detox/WebSocketClient.java b/detox/android/detox/src/main/java/com/wix/detox/WebSocketClient.java index 7140eba80b..bcc131241b 100644 --- a/detox/android/detox/src/main/java/com/wix/detox/WebSocketClient.java +++ b/detox/android/detox/src/main/java/com/wix/detox/WebSocketClient.java @@ -77,18 +77,27 @@ public void onPong(Buffer payload) { // empty } + private volatile boolean closing = false; + @Override public void onClose(int code, String reason) { Log.i(LOG_TAG, "At onClose"); Log.d(LOG_TAG, "Detox Closed: " + code + " " + reason); + closing = true; actionHandler.onClosed(); } public void close() { + if (closing) { + return; + } + closing = true; try { websocket.close(NORMAL_CLOSURE_STATUS, null); } catch (IOException e) { - Log.e(LOG_TAG, "WS close", e); + Log.i(LOG_TAG, "WS close", e); + } catch (IllegalStateException e) { + Log.i(LOG_TAG, "WS close", e); } } @@ -102,9 +111,6 @@ public void close() { private static final int NORMAL_CLOSURE_STATUS = 1000; - // TODO - // Need an API to stop the websocket from DetoxManager - public WebSocketClient(ActionHandler actionHandler) { this.actionHandler = actionHandler; } @@ -167,8 +173,7 @@ public void receiveAction(WebSocket webSocket, String json) { long messageId = object.getLong("messageId"); Log.d(LOG_TAG, "Detox Action Received: " + type); - // TODO - // This is just a dummy call now. Finish parsing params. + if (actionHandler != null) actionHandler.onAction(type, params.toString(), messageId); } catch (JSONException e) { Log.e(LOG_TAG, "Detox Error: receiveAction decode - " + e.toString()); diff --git a/detox/android/detox/src/main/java/com/wix/detox/espresso/DetoxAction.java b/detox/android/detox/src/main/java/com/wix/detox/espresso/DetoxAction.java index 791c6a96dc..954ddf5788 100644 --- a/detox/android/detox/src/main/java/com/wix/detox/espresso/DetoxAction.java +++ b/detox/android/detox/src/main/java/com/wix/detox/espresso/DetoxAction.java @@ -55,8 +55,6 @@ public float[] calculateCoordinates(View view) { Tap.SINGLE, c, Press.FINGER, InputDevice.SOURCE_UNKNOWN, MotionEvent.BUTTON_PRIMARY)); } - private final static int DEFAULT_SCROLL_DP = 500; - /** * Scrolls to the edge of the given scrollable view. * @@ -92,39 +90,31 @@ public void perform(UiController uiController, View view) { if (view instanceof AbsListView) { RNScrollListener l = new RNScrollListener((AbsListView) view); do { - ScrollHelper.perform(uiController, view, edge, DEFAULT_SCROLL_DP); + ScrollHelper.performOnce(uiController, view, edge); } while (l.didScroll()); l.cleanup(); - uiController.loopMainThreadUntilIdle(); - return; - } if (view instanceof ScrollView) { + } else if (view instanceof ScrollView) { int prevScrollY = view.getScrollY(); while (true) { - ScrollHelper.perform(uiController, view, edge, DEFAULT_SCROLL_DP); + ScrollHelper.performOnce(uiController, view, edge); int currentScrollY = view.getScrollY(); if (currentScrollY == prevScrollY) break; prevScrollY = currentScrollY; } - uiController.loopMainThreadUntilIdle(); - return; - } if (view instanceof HorizontalScrollView) { + } else if (view instanceof HorizontalScrollView) { int prevScrollX = view.getScrollX(); while (true) { - ScrollHelper.perform(uiController, view, edge, DEFAULT_SCROLL_DP); + ScrollHelper.performOnce(uiController, view, edge); int currentScrollX = view.getScrollX(); if (currentScrollX == prevScrollX) break; prevScrollX = currentScrollX; } - uiController.loopMainThreadUntilIdle(); - return; } else if (recyclerViewClass != null && recyclerViewClass.isInstance(view)) { RecyclerViewScrollListener l = new RecyclerViewScrollListener(view); do { - ScrollHelper.perform(uiController, view, edge, DEFAULT_SCROLL_DP); + ScrollHelper.performOnce(uiController, view, edge); } while (l.didScroll()); l.cleanup(); - uiController.loopMainThreadUntilIdle(); - return; } else { throw new RuntimeException( "Only descendants of AbsListView, ScrollView, HorizontalScrollView and RecyclerView are supported"); diff --git a/detox/android/detox/src/main/java/com/wix/detox/espresso/LooperIdlingResource.java b/detox/android/detox/src/main/java/com/wix/detox/espresso/LooperIdlingResource.java index 2b51e893aa..77adb88100 100644 --- a/detox/android/detox/src/main/java/com/wix/detox/espresso/LooperIdlingResource.java +++ b/detox/android/detox/src/main/java/com/wix/detox/espresso/LooperIdlingResource.java @@ -25,12 +25,23 @@ final public class LooperIdlingResource implements IdlingResource { private ResourceCallback resourceCallback; + private ResourceCallbackIdleHandler resIdleHandler = null; + public LooperIdlingResource(Looper monitoredLooper, boolean considerWaitIdle) { this.monitoredLooper = monitoredLooper; this.monitoredHandler = new Handler(monitoredLooper); this.considerWaitIdle = considerWaitIdle; } + /** + * Call this to properly stop the LooperIR. + */ + public void stop() { + if (resIdleHandler != null) { + resIdleHandler.stop = true; + } + } + // Only assigned and read from the main loop. private QueueInterrogator queueInterrogator; @@ -51,8 +62,11 @@ public boolean isIdleNow() { resourceCallback.onTransitionToIdle(); } } - Log.i(LOG_TAG, getName() + " looper is idle : " + String.valueOf(idle || idleWait)); - return idle || idleWait; + idle = idle || idleWait; + if (!idle) { + Log.i(LOG_TAG, getName() + " looper is busy"); + } + return idle; } @Override @@ -62,16 +76,17 @@ public void registerIdleTransitionCallback(ResourceCallback resourceCallback) { queueInterrogator = new QueueInterrogator(monitoredLooper); // must load idle handlers from monitored looper thread. - IdleHandler idleHandler = new ResourceCallbackIdleHandler(resourceCallback, queueInterrogator, + resIdleHandler = new ResourceCallbackIdleHandler(resourceCallback, queueInterrogator, monitoredHandler); - monitoredHandler.postAtFrontOfQueue(new Initializer(idleHandler)); + monitoredHandler.postAtFrontOfQueue(new Initializer(resIdleHandler)); } private static class ResourceCallbackIdleHandler implements IdleHandler { private final ResourceCallback resourceCallback; private final QueueInterrogator myInterrogator; private final Handler myHandler; + public volatile boolean stop = false; ResourceCallbackIdleHandler(ResourceCallback resourceCallback, QueueInterrogator myInterrogator, Handler myHandler) { @@ -93,7 +108,7 @@ public boolean queueIdle() { myHandler.sendEmptyMessage(-1); } - return true; + return !stop; } } diff --git a/detox/android/detox/src/main/java/com/wix/detox/espresso/ReactBridgeIdlingResource.java b/detox/android/detox/src/main/java/com/wix/detox/espresso/ReactBridgeIdlingResource.java index 6f7f44ba00..37ed9d0126 100644 --- a/detox/android/detox/src/main/java/com/wix/detox/espresso/ReactBridgeIdlingResource.java +++ b/detox/android/detox/src/main/java/com/wix/detox/espresso/ReactBridgeIdlingResource.java @@ -29,7 +29,9 @@ public String getName() { @Override public boolean isIdleNow() { boolean ret = idleNow.get(); - Log.i(LOG_TAG, "JS Bridge is idle : " + String.valueOf(ret)); + if (!ret) { + Log.i(LOG_TAG, "JS Bridge is busy"); + } return ret; } @@ -44,12 +46,12 @@ public void onTransitionToBridgeIdle() { if (callback != null) { callback.onTransitionToIdle(); } - Log.i(LOG_TAG, "JS Bridge transitions to idle."); + // Log.i(LOG_TAG, "JS Bridge transitions to idle."); } //Proxy calls it public void onTransitionToBridgeBusy() { idleNow.set(false); - Log.i(LOG_TAG, "JS Bridge transitions to busy."); + // Log.i(LOG_TAG, "JS Bridge transitions to busy."); } } diff --git a/detox/android/detox/src/main/java/com/wix/detox/espresso/ReactNativeTimersIdlingResource.java b/detox/android/detox/src/main/java/com/wix/detox/espresso/ReactNativeTimersIdlingResource.java index 3645ee89db..7a68e576c9 100644 --- a/detox/android/detox/src/main/java/com/wix/detox/espresso/ReactNativeTimersIdlingResource.java +++ b/detox/android/detox/src/main/java/com/wix/detox/espresso/ReactNativeTimersIdlingResource.java @@ -109,13 +109,13 @@ public boolean isIdleNow() { if (callback != null) { callback.onTransitionToIdle(); } - Log.i(LOG_TAG, "JS Timer is idle: true"); + // Log.i(LOG_TAG, "JS Timer is idle: true"); return true; } } Choreographer.getInstance().postFrameCallback(this); - Log.i(LOG_TAG, "JS Timer is idle: false"); + Log.i(LOG_TAG, "JS Timer is busy"); return false; } catch (ReflectException e) { Log.e(LOG_TAG, "Can't set up RN timer listener", e.getCause()); diff --git a/detox/android/detox/src/main/java/com/wix/detox/espresso/ReactNativeUIModuleIdlingResource.java b/detox/android/detox/src/main/java/com/wix/detox/espresso/ReactNativeUIModuleIdlingResource.java index 13f632931b..829461c685 100644 --- a/detox/android/detox/src/main/java/com/wix/detox/espresso/ReactNativeUIModuleIdlingResource.java +++ b/detox/android/detox/src/main/java/com/wix/detox/espresso/ReactNativeUIModuleIdlingResource.java @@ -113,7 +113,7 @@ public boolean isIdleNow() { if (callback != null) { callback.onTransitionToIdle(); } - Log.i(LOG_TAG, "UIManagerModule is idle."); + // Log.i(LOG_TAG, "UIManagerModule is idle."); return true; } diff --git a/detox/android/detox/src/main/java/com/wix/detox/espresso/ScrollHelper.java b/detox/android/detox/src/main/java/com/wix/detox/espresso/ScrollHelper.java index 60a79199ec..e396b775fe 100644 --- a/detox/android/detox/src/main/java/com/wix/detox/espresso/ScrollHelper.java +++ b/detox/android/detox/src/main/java/com/wix/detox/espresso/ScrollHelper.java @@ -175,4 +175,44 @@ private static float[][] interpolate(float[] start, float[] end, int steps) { return res; } + + /** + * Scrolls the View in a direction once by the maximum amount possible. (Till the edge + * of the screen.) + * + * Direction + * 1 -> left + * 2 -> Right + * 3 -> Up + * 4 -> Down + * + * @param direction Direction to scroll + */ + public static void performOnce(UiController uiController, View view, int direction) { + int adjWidth = 0; + int adjHeight = 0; + + int[] pos = new int[2]; + view.getLocationInWindow(pos); + + float[] screenSize = UiAutomatorHelper.getScreenSizeInPX(); + + if (direction == 1) { + adjWidth = (int) ((screenSize[0] - pos[0]) * (1 - 2 * DEFAULT_DEADZONE_PERCENT)); + } else if (direction == 2) { + adjWidth = (int) ((pos[0] + view.getWidth()) * (1 - 2 * DEFAULT_DEADZONE_PERCENT)); + } else if (direction == 3) { + adjHeight = (int) ((screenSize[1] - pos[1]) * (1 - 2 * DEFAULT_DEADZONE_PERCENT)); + } else { + adjHeight = (int) ((pos[1] + view.getHeight()) * (1 - 2 * DEFAULT_DEADZONE_PERCENT)); + } + + if (direction == 1 || direction == 2) { + doScroll(uiController, view, direction, adjWidth); + } else { + doScroll(uiController, view, direction, adjHeight); + } + + uiController.loopMainThreadUntilIdle(); + } } diff --git a/detox/src/Detox.js b/detox/src/Detox.js index e585701eea..0f7d1b88bb 100644 --- a/detox/src/Detox.js +++ b/detox/src/Detox.js @@ -86,7 +86,7 @@ class Detox { async beforeEach(...testNameComponents) { this._currentTestNumber++; - if(this._artifactsPathsProvider !== undefined) { + if (this._artifactsPathsProvider !== undefined) { const testArtifactsPath = this._artifactsPathsProvider.createPathForTest(this._currentTestNumber, ...testNameComponents) this.device.setArtifactsDestination(testArtifactsPath); } diff --git a/detox/src/client/Client.js b/detox/src/client/Client.js index f4744509e6..46da48f197 100644 --- a/detox/src/client/Client.js +++ b/detox/src/client/Client.js @@ -8,6 +8,7 @@ class Client { this.ws = new AsyncWebSocket(config.server); this.slowInvocationStatusHandler = null; this.slowInvocationTimeout = argparse.getArgValue('debug-synchronization'); + this.successfulTestRun = true; // flag for cleanup } async connect() { @@ -29,7 +30,7 @@ class Client { async cleanup() { if (this.ws.isOpen()) { - await this.sendAction(new actions.Cleanup()); + await this.sendAction(new actions.Cleanup(this.successfulTestRun)); } } @@ -49,7 +50,12 @@ class Client { if (this.slowInvocationTimeout) { this.slowInvocationStatusHandler = this.slowInvocationStatus(); } - await this.sendAction(new actions.Invoke(invocation)); + try { + await this.sendAction(new actions.Invoke(invocation)); + } catch (err) { + this.successfulTestRun = false; + throw new Error(err); + } clearTimeout(this.slowInvocationStatusHandler); } @@ -61,7 +67,7 @@ class Client { } slowInvocationStatus() { - return setTimeout( async () => { + return setTimeout(async () => { const status = await this.currentStatus(); this.slowInvocationStatusHandler = this.slowInvocationStatus(); }, this.slowInvocationTimeout); diff --git a/detox/src/client/Client.test.js b/detox/src/client/Client.test.js index 22b657147d..1d37ec0023 100644 --- a/detox/src/client/Client.test.js +++ b/detox/src/client/Client.test.js @@ -44,7 +44,7 @@ describe('Client', () => { it(`openURL() - should send an 'openURL' action and resolve when 'openURLDone returns' `, async () => { await connect(); client.ws.send.mockReturnValueOnce(response("openURLDone", {}, 1)); - await client.openURL({url:'url'}); + await client.openURL({url: 'url'}); expect(client.ws.send).toHaveBeenCalledTimes(2); }); @@ -75,7 +75,7 @@ describe('Client', () => { it(`execute() - "invokeResult" on an invocation object should resolve`, async () => { await connect(); - client.ws.send.mockReturnValueOnce(response("invokeResult", {result:"(GREYElementInteraction)"}, 1)); + client.ws.send.mockReturnValueOnce(response("invokeResult", {result: "(GREYElementInteraction)"}, 1)); const call = invoke.call(invoke.IOS.Class('GREYMatchers'), 'matcherForAccessibilityLabel:', 'test'); await client.execute(call()); @@ -107,7 +107,7 @@ describe('Client', () => { it(`execute() - "invokeResult" on an invocation function should resolve`, async () => { await connect(); - client.ws.send.mockReturnValueOnce(response("invokeResult", {result:"(GREYElementInteraction)"} ,1)); + client.ws.send.mockReturnValueOnce(response("invokeResult", {result: "(GREYElementInteraction)"}, 1)); const call = invoke.call(invoke.IOS.Class('GREYMatchers'), 'matcherForAccessibilityLabel:', 'test'); await client.execute(call); @@ -158,14 +158,13 @@ describe('Client', () => { function response(type, params, messageId) { return Promise.resolve( JSON.stringify({ - type: type, - params: params, - messageId: messageId - }) - )} + type: type, + params: params, + messageId: messageId + })); + } async function timeout(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } - }); diff --git a/detox/src/client/actions/actions.js b/detox/src/client/actions/actions.js index 5c8ccbb015..f659a702f9 100644 --- a/detox/src/client/actions/actions.js +++ b/detox/src/client/actions/actions.js @@ -13,7 +13,6 @@ class Action { throw new Error(`was expecting '${type}' , got ${JSON.stringify(response)}`); } } - } class Login extends Action { @@ -51,8 +50,11 @@ class ReloadReactNative extends Action { } class Cleanup extends Action { - constructor() { - super('cleanup'); + constructor(stopRunner) { + const params = { + stopRunner: stopRunner + }; + super('cleanup', params); } async handle(response) { diff --git a/detox/src/devices/EmulatorDriver.js b/detox/src/devices/EmulatorDriver.js index 448e0fe0db..9042f71afd 100644 --- a/detox/src/devices/EmulatorDriver.js +++ b/detox/src/devices/EmulatorDriver.js @@ -83,17 +83,12 @@ class EmulatorDriver extends DeviceDriverBase { //}); } - async terminate(deviceId, bundleId) { this.terminateInstrumentation(); await this.adbCmd(deviceId, `shell am force-stop ${bundleId}`); //await exec(`adb -s ${deviceId} shell am force-stop ${bundleId}`); } - async cleanup(deviceId, bundleId) { - this.terminateInstrumentation(); - } - terminateInstrumentation() { if (this.instrumentationProcess) { this.instrumentationProcess.kill('SIGHUP'); @@ -107,6 +102,10 @@ class EmulatorDriver extends DeviceDriverBase { } } + async cleanup(deviceId, bundleId) { + this.terminateInstrumentation(); + } + defaultLaunchArgsPrefix() { return '-e '; }