diff --git a/detox/package.json b/detox/package.json index ddaed93651..a8ae10432a 100644 --- a/detox/package.json +++ b/detox/package.json @@ -78,7 +78,8 @@ "/node_modules/", ".*Driver.js", "DeviceDriverBase.js", - "GREYConfiguration.js" + "GREYConfiguration.js", + "src/ios/earlgreyapi" ], "resetMocks": true, "resetModules": true, diff --git a/detox/src/invoke.js b/detox/src/invoke.js index a240b8693c..e8da6d4cd6 100644 --- a/detox/src/invoke.js +++ b/detox/src/invoke.js @@ -18,5 +18,6 @@ module.exports = { Espresso: Espresso.target, IOS: Invoke.genericInvokeObject, Android: Invoke.genericInvokeObject, - call: Invoke.call + call: Invoke.call, + callDirectly: Invoke.callDirectly }; diff --git a/detox/src/invoke/Invoke.js b/detox/src/invoke/Invoke.js index dca78d12e2..63f9fda1e9 100644 --- a/detox/src/invoke/Invoke.js +++ b/detox/src/invoke/Invoke.js @@ -22,6 +22,13 @@ function call(target, method, ...args) { }; } +function callDirectly(json) { + return { + type: 'Invocation', + value: json + }; +} + const genericInvokeObject = new Proxy({}, { get: (target, prop) => { @@ -36,5 +43,6 @@ const genericInvokeObject = new Proxy({}, module.exports = { call, - genericInvokeObject + callDirectly, + genericInvokeObject, }; diff --git a/detox/src/ios/earlgreyapi/GREYActions.js b/detox/src/ios/earlgreyapi/GREYActions.js new file mode 100644 index 0000000000..4631a69344 --- /dev/null +++ b/detox/src/ios/earlgreyapi/GREYActions.js @@ -0,0 +1,791 @@ +/** + + This code is generated. + For more information see generation/README.md. +*/ + + +// Globally declared helpers + +function sanitize_greyDirection(action) { + switch (action) { + case "left": + return 1; + case "right": + return 2; + case "up": + return 3; + case "down": + return 4; + + default: + throw new Error(`GREYAction.GREYDirection must be a 'left'/'right'/'up'/'down', got ${action}`); + } +} + + +class GREYActions { + /*@return A GREYAction that performs multiple taps of a specified @c count. +*/static actionForMultipleTapsWithCount(count) { + if (typeof count !== "number") throw new Error("count should be a number, but got " + (count + (" (" + (typeof count + ")")))); + return { + target: { + type: "Class", + value: "GREYActions" + }, + method: "actionForMultipleTapsWithCount:", + args: [{ + type: "NSInteger", + value: count + }] + }; + } + + /*@return A GREYAction that performs multiple taps of a specified @c count at a specified +@c point. +*/static actionForMultipleTapsWithCountAtPoint(count, point) { + if (typeof count !== "number") throw new Error("count should be a number, but got " + (count + (" (" + (typeof count + ")")))); + if (typeof point !== "object") throw new Error("point should be a object, but got " + (point + (" (" + (typeof point + ")")))); + if (typeof point.x !== "number") throw new Error("point.x should be a number, but got " + (point.x + (" (" + (typeof point.x + ")")))); + if (typeof point.y !== "number") throw new Error("point.y should be a number, but got " + (point.y + (" (" + (typeof point.y + ")")))); + return { + target: { + type: "Class", + value: "GREYActions" + }, + method: "actionForMultipleTapsWithCount:atPoint:", + args: [{ + type: "NSInteger", + value: count + }, { + type: "CGPoint", + value: point + }] + }; + } + + /*Returns an action that holds down finger for 1.0 second (@c kGREYLongPressDefaultDuration) to +simulate a long press. + +@return A GREYAction that performs a long press on an element. +*/static actionForLongPress() { + return { + target: { + type: "Class", + value: "GREYActions" + }, + method: "actionForLongPress", + args: [] + }; + } + + /*Returns an action that holds down finger for specified @c duration to simulate a long press. + +@param duration The duration of the long press. + +@return A GREYAction that performs a long press on an element. +*/static actionForLongPressWithDuration(duration) { + if (typeof duration !== "number") throw new Error("duration should be a number, but got " + (duration + (" (" + (typeof duration + ")")))); + return { + target: { + type: "Class", + value: "GREYActions" + }, + method: "actionForLongPressWithDuration:", + args: [{ + type: "CFTimeInterval", + value: duration + }] + }; + } + + /*Returns an action that holds down finger for specified @c duration at the specified @c point +(interpreted as being relative to the element) to simulate a long press. + +@param point The point that should be tapped. +@param duration The duration of the long press. + +@return A GREYAction that performs a long press on an element. +*/static actionForLongPressAtPointDuration(point, duration) { + if (typeof point !== "object") throw new Error("point should be a object, but got " + (point + (" (" + (typeof point + ")")))); + if (typeof point.x !== "number") throw new Error("point.x should be a number, but got " + (point.x + (" (" + (typeof point.x + ")")))); + if (typeof point.y !== "number") throw new Error("point.y should be a number, but got " + (point.y + (" (" + (typeof point.y + ")")))); + if (typeof duration !== "number") throw new Error("duration should be a number, but got " + (duration + (" (" + (typeof duration + ")")))); + return { + target: { + type: "Class", + value: "GREYActions" + }, + method: "actionForLongPressAtPoint:duration:", + args: [{ + type: "CGPoint", + value: point + }, { + type: "CFTimeInterval", + value: duration + }] + }; + } + + /*Returns an action that scrolls a @c UIScrollView by @c amount (in points) in the specified +@c direction. + +@param direction The direction of the swipe. +@param amount The amount of points in CGPoints to scroll. + +@return A GREYAction that scrolls a scroll view in a given @c direction for a given @c amount. +*/static actionForScrollInDirectionAmount(direction, amount) { + if (!["left", "right", "up", "down"].some(option => option === direction)) throw new Error("direction should be one of [left, right, up, down], but got " + direction); + if (typeof amount !== "number") throw new Error("amount should be a number, but got " + (amount + (" (" + (typeof amount + ")")))); + return { + target: { + type: "Class", + value: "GREYActions" + }, + method: "actionForScrollInDirection:amount:", + args: [{ + type: "GREYDirection", + value: sanitize_greyDirection(direction) + }, { + type: "CGFloat", + value: amount + }] + }; + } + + /*Returns a scroll action that scrolls in a @c direction for an @c amount of points starting from +the given start point specified as percentages. @c xOriginStartPercentage is the x start +position as a percentage of the total width of the scrollable visible area, +@c yOriginStartPercentage is the y start position as a percentage of the total height of the +scrollable visible area. @c xOriginStartPercentage and @c yOriginStartPercentage must be between +0 and 1, exclusive. + +@param direction The direction of the scroll. +@param amount The amount scroll in points to inject. +@param xOriginStartPercentage X coordinate of the start point specified as a percentage (0, 1) +exclusive, of the total width of the scrollable visible area. +@param yOriginStartPercentage Y coordinate of the start point specified as a percentage (0, 1) +exclusive, of the total height of the scrollable visible area. + +@return A GREYAction that scrolls a scroll view in a given @c direction for a given @c amount +starting from the given start points. +*/static actionForScrollInDirectionAmountXOriginStartPercentageYOriginStartPercentage(direction, amount, xOriginStartPercentage, yOriginStartPercentage) { + if (!["left", "right", "up", "down"].some(option => option === direction)) throw new Error("direction should be one of [left, right, up, down], but got " + direction); + if (typeof amount !== "number") throw new Error("amount should be a number, but got " + (amount + (" (" + (typeof amount + ")")))); + if (typeof xOriginStartPercentage !== "number") throw new Error("xOriginStartPercentage should be a number, but got " + (xOriginStartPercentage + (" (" + (typeof xOriginStartPercentage + ")")))); + if (typeof yOriginStartPercentage !== "number") throw new Error("yOriginStartPercentage should be a number, but got " + (yOriginStartPercentage + (" (" + (typeof yOriginStartPercentage + ")")))); + return { + target: { + type: "Class", + value: "GREYActions" + }, + method: "actionForScrollInDirection:amount:xOriginStartPercentage:yOriginStartPercentage:", + args: [{ + type: "GREYDirection", + value: sanitize_greyDirection(direction) + }, { + type: "CGFloat", + value: amount + }, { + type: "CGFloat", + value: xOriginStartPercentage + }, { + type: "CGFloat", + value: yOriginStartPercentage + }] + }; + } + + /*@return A GREYAction that scrolls to the given content @c edge of a scroll view. +*/static actionForScrollToContentEdge(edge) { + if (!["left", "right", "top", "bottom"].some(option => option === edge)) throw new Error("edge should be one of [left, right, top, bottom], but got " + edge); + return { + target: { + type: "Class", + value: "GREYActions" + }, + method: "actionForScrollToContentEdge:", + args: [{ + type: "GREYContentEdge", + value: edge + }] + }; + } + + /*A GREYAction that scrolls to the given content @c edge of a scroll view with the scroll action +starting from the given start point specified as percentages. @c xOriginStartPercentage is the x +start position as a percentage of the total width of the scrollable visible area, +@c yOriginStartPercentage is the y start position as a percentage of the total height of the +scrollable visible area. @c xOriginStartPercentage and @c yOriginStartPercentage must be between +0 and 1, exclusive. + +@param edge The edge towards which the scrolling is to take place. +@param xOriginStartPercentage X coordinate of the start point specified as a percentage (0, 1) +exclusive, of the total width of the scrollable visible area. +@param yOriginStartPercentage Y coordinate of the start point specified as a percentage (0, 1) +exclusive, of the total height of the scrollable visible area. + +@return A GREYAction that scrolls to the given content @c edge of a scroll view with the scroll +action starting from the given start point. +*/static actionForScrollToContentEdgeXOriginStartPercentageYOriginStartPercentage(edge, xOriginStartPercentage, yOriginStartPercentage) { + if (!["left", "right", "top", "bottom"].some(option => option === edge)) throw new Error("edge should be one of [left, right, top, bottom], but got " + edge); + if (typeof xOriginStartPercentage !== "number") throw new Error("xOriginStartPercentage should be a number, but got " + (xOriginStartPercentage + (" (" + (typeof xOriginStartPercentage + ")")))); + if (typeof yOriginStartPercentage !== "number") throw new Error("yOriginStartPercentage should be a number, but got " + (yOriginStartPercentage + (" (" + (typeof yOriginStartPercentage + ")")))); + return { + target: { + type: "Class", + value: "GREYActions" + }, + method: "actionForScrollToContentEdge:xOriginStartPercentage:yOriginStartPercentage:", + args: [{ + type: "GREYContentEdge", + value: edge + }, { + type: "CGFloat", + value: xOriginStartPercentage + }, { + type: "CGFloat", + value: yOriginStartPercentage + }] + }; + } + + /*Returns an action that fast swipes through the view. The start point of the swipe is chosen to +achieve the maximum the swipe possible to the other edge. + +@param direction The direction of the swipe. + +@return A GREYAction that performs a fast swipe in the given direction. +*/static actionForSwipeFastInDirection(direction) { + if (!["left", "right", "up", "down"].some(option => option === direction)) throw new Error("direction should be one of [left, right, up, down], but got " + direction); + return { + target: { + type: "Class", + value: "GREYActions" + }, + method: "actionForSwipeFastInDirection:", + args: [{ + type: "GREYDirection", + value: sanitize_greyDirection(direction) + }] + }; + } + + /*Returns an action that slow swipes through the view. The start point of the swipe is chosen to +achieve maximum the swipe possible to the other edge. + +@param direction The direction of the swipe. + +@return A GREYAction that performs a slow swipe in the given direction. +*/static actionForSwipeSlowInDirection(direction) { + if (!["left", "right", "up", "down"].some(option => option === direction)) throw new Error("direction should be one of [left, right, up, down], but got " + direction); + return { + target: { + type: "Class", + value: "GREYActions" + }, + method: "actionForSwipeSlowInDirection:", + args: [{ + type: "GREYDirection", + value: sanitize_greyDirection(direction) + }] + }; + } + + /*Returns an action that swipes through the view quickly in the given @c direction from a specific +origin. + +@param direction The direction of the swipe. +@param xOriginStartPercentage the x start position as a percentage of the total width +of the view. This must be between 0 and 1. +@param yOriginStartPercentage the y start position as a percentage of the total height +of the view. This must be between 0 and 1. + +@return A GREYAction that performs a fast swipe through a view in a specific direction from +the specified point. +*/static actionForSwipeFastInDirectionXOriginStartPercentageYOriginStartPercentage(direction, xOriginStartPercentage, yOriginStartPercentage) { + if (!["left", "right", "up", "down"].some(option => option === direction)) throw new Error("direction should be one of [left, right, up, down], but got " + direction); + if (typeof xOriginStartPercentage !== "number") throw new Error("xOriginStartPercentage should be a number, but got " + (xOriginStartPercentage + (" (" + (typeof xOriginStartPercentage + ")")))); + if (typeof yOriginStartPercentage !== "number") throw new Error("yOriginStartPercentage should be a number, but got " + (yOriginStartPercentage + (" (" + (typeof yOriginStartPercentage + ")")))); + return { + target: { + type: "Class", + value: "GREYActions" + }, + method: "actionForSwipeFastInDirection:xOriginStartPercentage:yOriginStartPercentage:", + args: [{ + type: "GREYDirection", + value: sanitize_greyDirection(direction) + }, { + type: "CGFloat", + value: xOriginStartPercentage + }, { + type: "CGFloat", + value: yOriginStartPercentage + }] + }; + } + + /*Returns an action that swipes through the view quickly in the given @c direction from a +specific origin. + +@param direction The direction of the swipe. +@param xOriginStartPercentage the x start position as a percentage of the total width +of the view. This must be between 0 and 1. +@param yOriginStartPercentage the y start position as a percentage of the total height +of the view. This must be between 0 and 1. + +@return A GREYAction that performs a slow swipe through a view in a specific direction from +the specified point. +*/static actionForSwipeSlowInDirectionXOriginStartPercentageYOriginStartPercentage(direction, xOriginStartPercentage, yOriginStartPercentage) { + if (!["left", "right", "up", "down"].some(option => option === direction)) throw new Error("direction should be one of [left, right, up, down], but got " + direction); + if (typeof xOriginStartPercentage !== "number") throw new Error("xOriginStartPercentage should be a number, but got " + (xOriginStartPercentage + (" (" + (typeof xOriginStartPercentage + ")")))); + if (typeof yOriginStartPercentage !== "number") throw new Error("yOriginStartPercentage should be a number, but got " + (yOriginStartPercentage + (" (" + (typeof yOriginStartPercentage + ")")))); + return { + target: { + type: "Class", + value: "GREYActions" + }, + method: "actionForSwipeSlowInDirection:xOriginStartPercentage:yOriginStartPercentage:", + args: [{ + type: "GREYDirection", + value: sanitize_greyDirection(direction) + }, { + type: "CGFloat", + value: xOriginStartPercentage + }, { + type: "CGFloat", + value: yOriginStartPercentage + }] + }; + } + + /*Returns an action that performs a multi-finger slow swipe through the view in the given +@c direction. + +@param direction The direction of the swipe. +@param numberOfFingers Number of fingers touching the screen for the swipe. + +@return A GREYAction that performs a multi-finger slow swipe through a view in a specific +direction from the specified point. +*/static actionForMultiFingerSwipeSlowInDirectionNumberOfFingers(direction, numberOfFingers) { + if (!["left", "right", "up", "down"].some(option => option === direction)) throw new Error("direction should be one of [left, right, up, down], but got " + direction); + if (typeof numberOfFingers !== "number") throw new Error("numberOfFingers should be a number, but got " + (numberOfFingers + (" (" + (typeof numberOfFingers + ")")))); + return { + target: { + type: "Class", + value: "GREYActions" + }, + method: "actionForMultiFingerSwipeSlowInDirection:numberOfFingers:", + args: [{ + type: "GREYDirection", + value: sanitize_greyDirection(direction) + }, { + type: "NSInteger", + value: numberOfFingers + }] + }; + } + + /*Returns an action that performs a multi-finger fast swipe through the view in the given +@c direction. + +@param direction The direction of the swipe. +@param numberOfFingers Number of fingers touching the screen for the swipe. + +@return A GREYAction that performs a multi-finger fast swipe through a view in a specific +direction from the specified point. +*/static actionForMultiFingerSwipeFastInDirectionNumberOfFingers(direction, numberOfFingers) { + if (!["left", "right", "up", "down"].some(option => option === direction)) throw new Error("direction should be one of [left, right, up, down], but got " + direction); + if (typeof numberOfFingers !== "number") throw new Error("numberOfFingers should be a number, but got " + (numberOfFingers + (" (" + (typeof numberOfFingers + ")")))); + return { + target: { + type: "Class", + value: "GREYActions" + }, + method: "actionForMultiFingerSwipeFastInDirection:numberOfFingers:", + args: [{ + type: "GREYDirection", + value: sanitize_greyDirection(direction) + }, { + type: "NSInteger", + value: numberOfFingers + }] + }; + } + + /*Returns an action that performs a multi-finger slow swipe through the view in the given +@c direction. + +@param direction The direction of the swipe. +@param numberOfFingers Number of fingers touching the screen for the swipe. + +@return A GREYAction that performs a multi-finger slow swipe through a view in a specific +direction from the specified point. +*/static actionForMultiFingerSwipeSlowInDirectionNumberOfFingersXOriginStartPercentageYOriginStartPercentage(direction, numberOfFingers, xOriginStartPercentage, yOriginStartPercentage) { + if (!["left", "right", "up", "down"].some(option => option === direction)) throw new Error("direction should be one of [left, right, up, down], but got " + direction); + if (typeof numberOfFingers !== "number") throw new Error("numberOfFingers should be a number, but got " + (numberOfFingers + (" (" + (typeof numberOfFingers + ")")))); + if (typeof xOriginStartPercentage !== "number") throw new Error("xOriginStartPercentage should be a number, but got " + (xOriginStartPercentage + (" (" + (typeof xOriginStartPercentage + ")")))); + if (typeof yOriginStartPercentage !== "number") throw new Error("yOriginStartPercentage should be a number, but got " + (yOriginStartPercentage + (" (" + (typeof yOriginStartPercentage + ")")))); + return { + target: { + type: "Class", + value: "GREYActions" + }, + method: "actionForMultiFingerSwipeSlowInDirection:numberOfFingers:xOriginStartPercentage:yOriginStartPercentage:", + args: [{ + type: "GREYDirection", + value: sanitize_greyDirection(direction) + }, { + type: "NSInteger", + value: numberOfFingers + }, { + type: "CGFloat", + value: xOriginStartPercentage + }, { + type: "CGFloat", + value: yOriginStartPercentage + }] + }; + } + + /*Returns an action that performs a multi-finger fast swipe through the view in the given +@c direction. + +@param direction The direction of the swipe. +@param numberOfFingers Number of fingers touching the screen for the swipe. + +@return A GREYAction that performs a multi-finger fast swipe through a view in a specific +direction from the specified point. +*/static actionForMultiFingerSwipeFastInDirectionNumberOfFingersXOriginStartPercentageYOriginStartPercentage(direction, numberOfFingers, xOriginStartPercentage, yOriginStartPercentage) { + if (!["left", "right", "up", "down"].some(option => option === direction)) throw new Error("direction should be one of [left, right, up, down], but got " + direction); + if (typeof numberOfFingers !== "number") throw new Error("numberOfFingers should be a number, but got " + (numberOfFingers + (" (" + (typeof numberOfFingers + ")")))); + if (typeof xOriginStartPercentage !== "number") throw new Error("xOriginStartPercentage should be a number, but got " + (xOriginStartPercentage + (" (" + (typeof xOriginStartPercentage + ")")))); + if (typeof yOriginStartPercentage !== "number") throw new Error("yOriginStartPercentage should be a number, but got " + (yOriginStartPercentage + (" (" + (typeof yOriginStartPercentage + ")")))); + return { + target: { + type: "Class", + value: "GREYActions" + }, + method: "actionForMultiFingerSwipeFastInDirection:numberOfFingers:xOriginStartPercentage:yOriginStartPercentage:", + args: [{ + type: "GREYDirection", + value: sanitize_greyDirection(direction) + }, { + type: "NSInteger", + value: numberOfFingers + }, { + type: "CGFloat", + value: xOriginStartPercentage + }, { + type: "CGFloat", + value: yOriginStartPercentage + }] + }; + } + + /*Returns an action that pinches view quickly in the specified @c direction and @c angle. + +@param pinchDirection The direction of the pinch action. +@param angle The angle of the pinch action in radians. +Use @c kGREYPinchAngleDefault for the default angle (currently set to +30 degrees). + +@return A GREYAction that performs a fast pinch on the view in the specified @c direction. +*/static actionForPinchFastInDirectionWithAngle(pinchDirection, angle) { + if (!["outward", "inward"].some(option => option === pinchDirection)) throw new Error("pinchDirection should be one of [outward, inward], but got " + pinchDirection); + if (typeof angle !== "number") throw new Error("angle should be a number, but got " + (angle + (" (" + (typeof angle + ")")))); + return { + target: { + type: "Class", + value: "GREYActions" + }, + method: "actionForPinchFastInDirection:withAngle:", + args: [{ + type: "GREYPinchDirection", + value: pinchDirection + }, { + type: "double", + value: angle + }] + }; + } + + /*Returns an action that pinches view slowly in the specified @c direction and @c angle. + +@param pinchDirection The direction of the pinch action. +@param angle The angle of the pinch action in radians. +Use @c kGREYPinchAngleDefault for the default angle (currently set to +30 degrees). + +@return A GREYAction that performs a slow pinch on the view in the specified @c direction. +*/static actionForPinchSlowInDirectionWithAngle(pinchDirection, angle) { + if (!["outward", "inward"].some(option => option === pinchDirection)) throw new Error("pinchDirection should be one of [outward, inward], but got " + pinchDirection); + if (typeof angle !== "number") throw new Error("angle should be a number, but got " + (angle + (" (" + (typeof angle + ")")))); + return { + target: { + type: "Class", + value: "GREYActions" + }, + method: "actionForPinchSlowInDirection:withAngle:", + args: [{ + type: "GREYPinchDirection", + value: pinchDirection + }, { + type: "double", + value: angle + }] + }; + } + + /*Returns an action that attempts to move slider to within 1.0e-6f values of @c value. + +@param value The value to which the slider should be moved. If this is not attainable after a +reasonable number of attempts (currently 10) we assume that the @c value is +unattainable for a user (it is probably the case this value resides between two +pixels). In this case, the slider will end up at a user attainable value +that is closest to @c value. + +@return A GREYAction that moves a slider to a given @c value. +*/static actionForMoveSliderToValue(value) { + if (typeof value !== "number") throw new Error("value should be a number, but got " + (value + (" (" + (typeof value + ")")))); + return { + target: { + type: "Class", + value: "GREYActions" + }, + method: "actionForMoveSliderToValue:", + args: [{ + type: "float", + value: value + }] + }; + } + + /*Returns an action that changes the value of UIStepper to @c value by tapping the appropriate +button multiple times. + +@param value The value to change the UIStepper to. + +@return A GREYAction that sets the given @c value on a stepper. +*/static actionForSetStepperValue(value) { + if (typeof value !== "number") throw new Error("value should be a number, but got " + (value + (" (" + (typeof value + ")")))); + return { + target: { + type: "Class", + value: "GREYActions" + }, + method: "actionForSetStepperValue:", + args: [{ + type: "double", + value: value + }] + }; + } + + /*Returns an action that taps on an element at the activation point of the element. + +@return A GREYAction to tap on an element. +*/static actionForTap() { + return { + target: { + type: "Class", + value: "GREYActions" + }, + method: "actionForTap", + args: [] + }; + } + + /*Returns an action that taps on an element at the specified @c point. + +@param point The point that should be tapped. It must be in the coordinate system of the +element and it's position is relative to the origin of the element, as in +(element_width/2, element_height/2) will tap at the center of element. + +@return A GREYAction to tap on an element at a specific point. +*/static actionForTapAtPoint(point) { + if (typeof point !== "object") throw new Error("point should be a object, but got " + (point + (" (" + (typeof point + ")")))); + if (typeof point.x !== "number") throw new Error("point.x should be a number, but got " + (point.x + (" (" + (typeof point.x + ")")))); + if (typeof point.y !== "number") throw new Error("point.y should be a number, but got " + (point.y + (" (" + (typeof point.y + ")")))); + return { + target: { + type: "Class", + value: "GREYActions" + }, + method: "actionForTapAtPoint:", + args: [{ + type: "CGPoint", + value: point + }] + }; + } + + /*Returns an action that uses the iOS keyboard to input a string. + +@param text The text to be typed. For Objective-C, backspace is supported by using "\b" in the +string and "\u{8}" in Swift strings. Return key is supported with "\n". +For Example: @"Helpo\b\bloWorld" will type HelloWorld in Objective-C. +"Helpo\u{8}\u{8}loWorld" will type HelloWorld in Swift. + +@return A GREYAction to type a specific text string in a text field. +*/static actionForTypeText(text) { + if (typeof text !== "string") throw new Error("text should be a string, but got " + (text + (" (" + (typeof text + ")")))); + return { + target: { + type: "Class", + value: "GREYActions" + }, + method: "actionForTypeText:", + args: [{ + type: "NSString", + value: text + }] + }; + } + + /*Returns an action that sets text on a UITextField or webview input directly. + +@param text The text to be typed. + +@return A GREYAction to type a specific text string in a text field. +*/static actionForReplaceText(text) { + if (typeof text !== "string") throw new Error("text should be a string, but got " + (text + (" (" + (typeof text + ")")))); + return { + target: { + type: "Class", + value: "GREYActions" + }, + method: "actionForReplaceText:", + args: [{ + type: "NSString", + value: text + }] + }; + } + + /*@return A GREYAction that clears a text field by injecting back-spaces. +*/static actionForClearText() { + return { + target: { + type: "Class", + value: "GREYActions" + }, + method: "actionForClearText", + args: [] + }; + } + + /*Returns an action that toggles a switch control. This action is applicable to all elements that +implement the selector UISwitch::isOn and include UISwitch controls. + +@param on The switch control state. + +@return A GREYAction to toggle a UISwitch. +*/static actionForTurnSwitchOn(on) { + if (typeof on !== "boolean") throw new Error("on should be a boolean, but got " + (on + (" (" + (typeof on + ")")))); + return { + target: { + type: "Class", + value: "GREYActions" + }, + method: "actionForTurnSwitchOn:", + args: [{ + type: "BOOL", + value: on + }] + }; + } + + /*Returns an action that injects dates/times into UIDatePickers. + +@param date The date to set the UIDatePicker. + +@return A GREYAction that sets a given date/time on a UIDatePicker. +*/static actionForSetDate(date) { + if (typeof date !== "number") throw new Error("date should be a number, but got " + (date + (" (" + (typeof date + ")")))); + return { + target: { + type: "Class", + value: "GREYActions" + }, + method: "actionForSetDate:", + args: [{ + type: "NSDate *", + value: date + }] + }; + } + + /*Returns an action that selects @c value on the given @c column of a UIPickerView. + +@param column The UIPickerView column being set. +@param value The value to set the UIPickerView. + +@return A GREYAction to set the value of a specified column of a UIPickerView. +*/static actionForSetPickerColumnToValue(column, value) { + if (typeof column !== "number") throw new Error("column should be a number, but got " + (column + (" (" + (typeof column + ")")))); + if (typeof value !== "string") throw new Error("value should be a string, but got " + (value + (" (" + (typeof value + ")")))); + return { + target: { + type: "Class", + value: "GREYActions" + }, + method: "actionForSetPickerColumn:toValue:", + args: [{ + type: "NSInteger", + value: column + }, { + type: "NSString", + value: value + }] + }; + } + + /*Returns an action that executes JavaScript against a UIWebView and sets the return value to +@c outResult if provided. + +@param js The Javascript code to be executed. +@param outResult The result of the code execution. + +@return A GREYAction that executes JavaScript code against a UIWebView. +*/static actionForJavaScriptExecutionOutput(js, outResult) { + if (typeof js !== "string") throw new Error("js should be a string, but got " + (js + (" (" + (typeof js + ")")))); + return { + target: { + type: "Class", + value: "GREYActions" + }, + method: "actionForJavaScriptExecution:output:", + args: [{ + type: "NSString", + value: js + }, { + type: "out __strong NSString **", + value: outResult + }] + }; + } + + /*Returns an action that takes a snapshot of the selected element. + +@param outImage The UIImage where the image content is stored. + +@return A GREYAction that takes a snapshot of an UI element. +*/static actionForSnapshot(outImage) { + return { + target: { + type: "Class", + value: "GREYActions" + }, + method: "actionForSnapshot:", + args: [{ + type: "out __strong UIImage **", + value: outImage + }] + }; + } + +} + +module.exports = GREYActions; \ No newline at end of file diff --git a/detox/src/ios/expect.js b/detox/src/ios/expect.js index 564845d95e..a1756ba35f 100644 --- a/detox/src/ios/expect.js +++ b/detox/src/ios/expect.js @@ -11,6 +11,7 @@ const ExistsMatcher = matchers.ExistsMatcher; const NotExistsMatcher = matchers.NotExistsMatcher; const TextMatcher = matchers.TextMatcher; const ValueMatcher = matchers.ValueMatcher; +const GreyActions = require('./earlgreyapi/GREYActions'); let invocationManager; @@ -45,29 +46,28 @@ class Action {} class TapAction extends Action { constructor() { super(); - this._call = invoke.call(invoke.IOS.Class('GREYActions'), 'actionForTap'); + this._call = invoke.callDirectly(GreyActions.actionForTap()); } } class TapAtPointAction extends Action { - constructor(value) { - super(); - this._call = invoke.call(invoke.IOS.Class('GREYActions'), 'actionForTapAtPoint:', invoke.IOS.CGPoint(value)); - } + constructor(value) { + super(); + this._call = invoke.callDirectly(GreyActions.actionForTapAtPoint(value)); + } } class LongPressAction extends Action { constructor() { super(); - this._call = invoke.call(invoke.IOS.Class('GREYActions'), 'actionForLongPress'); + this._call = invoke.callDirectly(GreyActions.actionForLongPress()); } } class MultiTapAction extends Action { constructor(value) { super(); - if (typeof value !== 'number') throw new Error(`MultiTapAction ctor argument must be a number, got ${typeof value}`); - this._call = invoke.call(invoke.IOS.Class('GREYActions'), 'actionForMultipleTapsWithCount:', invoke.IOS.NSInteger(value)); + this._call = invoke.callDirectly(GreyActions.actionForMultipleTapsWithCount(value)); } } @@ -130,34 +130,41 @@ class SwipeAction extends Action { super(); if (typeof direction !== 'string') throw new Error(`SwipeAction ctor 1st argument must be a string, got ${typeof direction}`); if (typeof speed !== 'string') throw new Error(`SwipeAction ctor 2nd argument must be a string, got ${typeof speed}`); - switch (direction) { - case 'left': direction = 1; break; - case 'right': direction = 2; break; - case 'up': direction = 3; break; - case 'down': direction = 4; break; - default: throw new Error(`SwipeAction direction must be a 'left'/'right'/'up'/'down', got ${direction}`); - } + if (percentage) { let x, y; const eps = 10 ** -8; switch (direction) { - case 1: x = percentage, y = eps; break; - case 2: x = percentage, y = eps; break; - case 3: y = percentage, x = eps; break; - case 4: y = percentage, x = eps; break; + case "left": + x = percentage, y = eps; + break; + case "right": + x = percentage, y = eps; + break; + case "up": + y = percentage, x = eps; + break; + case "down": + y = percentage, x = eps; + break; } + if (speed == 'fast') { - this._call = invoke.call(invoke.IOS.Class('GREYActions'), 'actionForSwipeFastInDirection:xOriginStartPercentage:yOriginStartPercentage:', invoke.IOS.NSInteger(direction), invoke.IOS.CGFloat(x), invoke.IOS.CGFloat(y)); + this._call = invoke.callDirectly( + GreyActions.actionForSwipeFastInDirectionXOriginStartPercentageYOriginStartPercentage(direction, x, y) + ); } else if (speed == 'slow') { - this._call = invoke.call(invoke.IOS.Class('GREYActions'), 'actionForSwipeSlowInDirection:xOriginStartPercentage:yOriginStartPercentage:', invoke.IOS.NSInteger(direction), invoke.IOS.CGFloat(x), invoke.IOS.CGFloat(y)); + this._call = invoke.callDirectly( + GreyActions.actionForSwipeSlowInDirectionXOriginStartPercentageYOriginStartPercentage(direction, x, y) + ); } else { throw new Error(`SwipeAction speed must be a 'fast'/'slow', got ${speed}`); } } else { if (speed == 'fast') { - this._call = invoke.call(invoke.IOS.Class('GREYActions'), 'actionForSwipeFastInDirection:', invoke.IOS.NSInteger(direction)); + this._call = invoke.callDirectly(GreyActions.actionForSwipeFastInDirection(direction)); } else if (speed == 'slow') { - this._call = invoke.call(invoke.IOS.Class('GREYActions'), 'actionForSwipeSlowInDirection:', invoke.IOS.NSInteger(direction)); + this._call = invoke.callDirectly(GreyActions.actionForSwipeSlowInDirection(direction)); } else { throw new Error(`SwipeAction speed must be a 'fast'/'slow', got ${speed}`); } diff --git a/generation/README.md b/generation/README.md new file mode 100644 index 0000000000..27985fd755 --- /dev/null +++ b/generation/README.md @@ -0,0 +1,9 @@ +# Generation + +This part of the repository aims to automate the adaption of the underlying testing frameworks. + +## Development + +- `npm install` +- `npm run build` builds every file specified in the `index.js` +- `npm test` runs integration level tests diff --git a/generation/__tests__/__snapshots__/earl-grey.js.snap b/generation/__tests__/__snapshots__/earl-grey.js.snap new file mode 100644 index 0000000000..d83bb7641b --- /dev/null +++ b/generation/__tests__/__snapshots__/earl-grey.js.snap @@ -0,0 +1,94 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`earl-grey generation Error handling should thow error for CGPoint with wrong x and y values 1`] = `"point.x should be a number, but got 3 (string)"`; + +exports[`earl-grey generation Error handling should thow error for CGPoint with wrong x and y values 2`] = `"point.y should be a number, but got undefined (undefined)"`; + +exports[`earl-grey generation Error handling should throw error for not in accepted range 1`] = `"direction should be one of [left, right, up, down], but got flipside"`; + +exports[`earl-grey generation Error handling should throw error for wrong type 1`] = `"count should be a number, but got foo (string)"`; + +exports[`earl-grey generation Error handling should throw error for wrong type 2`] = `"point should be a object, but got 4 (number)"`; + +exports[`earl-grey generation Invocations should return the invocation object for methods 1`] = ` +Object { + "args": Array [ + Object { + "type": "NSInteger", + "value": 3, + }, + ], + "method": "actionForMultipleTapsWithCount:", + "target": Object { + "type": "Class", + "value": "GREYActions", + }, +} +`; + +exports[`earl-grey generation Invocations should return the invocation object for methods with objects as args 1`] = ` +Object { + "args": Array [ + Object { + "type": "NSInteger", + "value": 3, + }, + Object { + "type": "CGPoint", + "value": Object { + "x": 3, + "y": 4, + }, + }, + ], + "method": "actionForMultipleTapsWithCount:atPoint:", + "target": Object { + "type": "Class", + "value": "GREYActions", + }, +} +`; + +exports[`earl-grey generation Invocations should return the invocation object for methods with strings 1`] = ` +Object { + "args": Array [ + Object { + "type": "NSString", + "value": "Foo", + }, + ], + "method": "actionForTypeText:", + "target": Object { + "type": "Class", + "value": "GREYActions", + }, +} +`; + +exports[`earl-grey generation Invocations should sanitize the directions 1`] = ` +Object { + "args": Array [ + Object { + "type": "GREYDirection", + "value": 4, + }, + Object { + "type": "CGFloat", + "value": 3, + }, + Object { + "type": "CGFloat", + "value": 4, + }, + Object { + "type": "CGFloat", + "value": 5, + }, + ], + "method": "actionForScrollInDirection:amount:xOriginStartPercentage:yOriginStartPercentage:", + "target": Object { + "type": "Class", + "value": "GREYActions", + }, +} +`; diff --git a/generation/__tests__/__snapshots__/global-functions.js.snap b/generation/__tests__/__snapshots__/global-functions.js.snap new file mode 100644 index 0000000000..7d66823cdd --- /dev/null +++ b/generation/__tests__/__snapshots__/global-functions.js.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`globals sanitize_greyDirection should fail with unknown value 1`] = `"GREYAction.GREYDirection must be a 'left'/'right'/'up'/'down', got kittens"`; diff --git a/generation/__tests__/earl-grey.js b/generation/__tests__/earl-grey.js new file mode 100644 index 0000000000..0f66ed3475 --- /dev/null +++ b/generation/__tests__/earl-grey.js @@ -0,0 +1,156 @@ +const fs = require("fs"); +const remove = require("remove"); +const earlGreyGenerator = require("../earl-grey"); + +describe("earl-grey generation", () => { + let ExampleClass; + let exampleContent; + beforeAll(() => { + // Generate the code to test + fs.mkdirSync("./__tests__/generated"); + + const files = { + "./fixtures/example.h": "./__tests__/generated/example.js" + }; + + console.log('==> generating earl grey files'); + earlGreyGenerator(files); + + console.log('==> loading earl grey files'); + // Load + ExampleClass = require("./generated/example.js"); + exampleContent = fs.readFileSync( + "./__tests__/generated/example.js", + "utf8" + ); + }); + + it("should export the class", () => { + expect(ExampleClass.actionForMultipleTapsWithCount).toBeInstanceOf( + Function + ); + }); + + describe("Comments", () => { + it("should include single line comments", () => { + expect(exampleContent.indexOf("Single Line Comment here")).not.toBe(-1); + }); + + it("should include multi line comments", () => { + expect( + exampleContent.indexOf("Multi Line Comment here\nAwesome") + ).not.toBe(-1); + }); + }); + + describe("Error handling", () => { + it("should throw error for wrong type", () => { + expect(() => { + ExampleClass.actionForMultipleTapsWithCount("foo"); + }).toThrowErrorMatchingSnapshot(); + + expect(() => { + ExampleClass.actionForMultipleTapsWithCount(42); + }).not.toThrow(); + + expect(() => { + ExampleClass.actionForMultipleTapsWithCountAtPoint(3, 4); + }).toThrowErrorMatchingSnapshot(); + + expect(() => { + ExampleClass.actionForMultipleTapsWithCountAtPoint(42, { x: 1, y: 2 }); + }).not.toThrow(); + }); + + it("should throw error for not in accepted range", () => { + expect(() => { + ExampleClass.actionForScrollInDirectionAmountXOriginStartPercentageYOriginStartPercentage( + "flipside", + 3, + 4, + 5 + ); + }).toThrowErrorMatchingSnapshot(); + + expect(() => { + ExampleClass.actionForScrollInDirectionAmountXOriginStartPercentageYOriginStartPercentage( + "down", + 3, + 4, + 5 + ); + }).not.toThrow(); + }); + + it("should thow error for CGPoint with wrong x and y values", () => { + expect(() => { + ExampleClass.actionForMultipleTapsWithCountAtPoint(3, {x: 3, y: 4}); + }).not.toThrow(); + + expect(() => { + ExampleClass.actionForMultipleTapsWithCountAtPoint(3, {x: "3", y: 4}); + }).toThrowErrorMatchingSnapshot(); + + expect(() => { + ExampleClass.actionForMultipleTapsWithCountAtPoint(3, {x: 3}); + }).toThrowErrorMatchingSnapshot(); + }); + }); + + describe("Invocations", () => { + it("should return the invocation object for methods", () => { + const result = ExampleClass.actionForMultipleTapsWithCount(3); + + expect(result.target.type).toBe('Class'); + expect(result.target.value).toBe('GREYActions'); + + expect(result.method).toBe('actionForMultipleTapsWithCount:'); + + expect(result.args.length).toBe(1); + expect(result.args[0].type).toBe('NSInteger'); + expect(result.args[0].value).toBe(3); + expect(result).toMatchSnapshot(); + }); + + it("should return the invocation object for methods with objects as args", () => { + const result = ExampleClass.actionForMultipleTapsWithCountAtPoint(3, {x: 3, y: 4}); + + expect(result.target.type).toBe('Class'); + expect(result.target.value).toBe('GREYActions'); + + expect(result.method).toBe('actionForMultipleTapsWithCount:atPoint:'); + + expect(result.args.length).toBe(2); + expect(result.args[0].type).toBe('NSInteger'); + expect(result.args[0].value).toBe(3); + expect(result.args[1].type).toBe('CGPoint'); + expect(result.args[1].value).toEqual({x: 3, y: 4}); + expect(result).toMatchSnapshot(); + }); + + it("should return the invocation object for methods with strings", () => { + const result = ExampleClass.actionForTypeText("Foo"); + + expect(result.args[0].type).toBe('NSString'); + expect(result).toMatchSnapshot(); + }); + + it("should sanitize the directions", () => { + const result = ExampleClass.actionForScrollInDirectionAmountXOriginStartPercentageYOriginStartPercentage( + "down", + 3, + 4, + 5 + ); + + expect(result.args[0].type).toBe("GREYDirection"); + expect(result.args[0].value).toBe(4); + expect(result).toMatchSnapshot(); + }); + }); + + afterAll(() => { + // Clean up + remove.removeSync("./__tests__/generated"); + }); +}); diff --git a/generation/__tests__/global-functions.js b/generation/__tests__/global-functions.js new file mode 100644 index 0000000000..a73429dcd5 --- /dev/null +++ b/generation/__tests__/global-functions.js @@ -0,0 +1,18 @@ +const globals = require("../earl-grey/global-functions"); + +describe("globals", () => { + describe("sanitize_greyDirection", () => { + it('should return numbers for strings', () => { + expect(globals.sanitize_greyDirection('left')).toBe(1); + expect(globals.sanitize_greyDirection("right")).toBe(2); + expect(globals.sanitize_greyDirection("up")).toBe(3); + expect(globals.sanitize_greyDirection("down")).toBe(4); + }); + + it('should fail with unknown value', () => { + expect(() => { + globals.sanitize_greyDirection("kittens"); + }).toThrowErrorMatchingSnapshot(); + }); + }); +}); \ No newline at end of file diff --git a/generation/earl-grey/global-functions.js b/generation/earl-grey/global-functions.js new file mode 100644 index 0000000000..c918e28556 --- /dev/null +++ b/generation/earl-grey/global-functions.js @@ -0,0 +1,21 @@ +// Globally declared helpers + +function sanitize_greyDirection(action) { + switch (action) { + case "left": + return 1; + case "right": + return 2; + case "up": + return 3; + case "down": + return 4; + + default: + throw new Error(`GREYAction.GREYDirection must be a 'left'/'right'/'up'/'down', got ${action}`); + } +} + +module.exports = { + sanitize_greyDirection, +}; \ No newline at end of file diff --git a/generation/earl-grey/index.js b/generation/earl-grey/index.js new file mode 100644 index 0000000000..0efa80fff5 --- /dev/null +++ b/generation/earl-grey/index.js @@ -0,0 +1,217 @@ +const t = require("babel-types"); +const objectiveCParser = require("objective-c-parser"); +const generate = require("babel-generator").default; +const fs = require("fs"); + +const { methodNameToSnakeCase } = require("../helpers"); + +const { + generateTypeCheck, + generateIsOneOfCheck +} = require("babel-generate-guard-clauses"); + +const isNumber = generateTypeCheck("number"); +const isString = generateTypeCheck("string"); +const isBoolean = generateTypeCheck("boolean"); +const isPoint = [ + generateTypeCheck("object"), + generateTypeCheck("number", { selector: "x" }), + generateTypeCheck("number", { selector: "y" }) +]; +const isOneOf = generateIsOneOfCheck; + +/** + * the input provided by objective-c-parser looks like this: + * { + * "name": "BasicName", + * "methods": [ + * { + * "args": [], + * "comment": "This is the comment of basic method one", + * "name": "basicMethodOne", + * "returnType": "NSInteger" + * }, + * { + * "args": [ + * { + * "type": "NSInteger", + * "name": "argOne" + * }, + * { + * "type": "NSString", + * "name": "argTwo" + * } + * ], + * "comment": "This is the comment of basic method two.\nIt has multiple lines", + * "name": "basicMethodTwoWithArgOneAndArgTwo", + * "returnType": "NSString" + * } + * ] + * } + */ +function createClass(json) { + return t.classDeclaration( + t.identifier(json.name), + null, + t.classBody(json.methods.map(createMethod.bind(null, json.name))), + [] + ); +} + +function createExport(json) { + return t.expressionStatement( + t.assignmentExpression( + "=", + t.memberExpression( + t.identifier("module"), + t.identifier("exports"), + false + ), + t.identifier(json.name) + ) + ); +} + +function createMethod(className, json) { + const m = t.classMethod( + "method", + t.identifier(methodNameToSnakeCase(json.name)), + json.args.map(({ name }) => t.identifier(name)), + t.blockStatement(createMethodBody(className, json)), + false, + json.static + ); + + if (json.comment) { + const comment = { + type: json.comment.indexOf("\n") === -1 ? "LineComment" : "BlockComment", + value: json.comment + "\n" + }; + + m.leadingComments = m.leadingComments || []; + m.leadingComments.push(comment); + } + return m; +} + +const supportedTypesMap = { + NSUInteger: "NSInteger", + "NSString *": "NSString" +}; + +function sanitizeArgumentType(json) { + if (supportedTypesMap[json.type]) { + return Object.assign({}, json, { + type: supportedTypesMap[json.type] + }); + } + return json; +} + +function createMethodBody(className, json) { + const sanitizedJson = Object.assign({}, json, { + args: json.args.map(argJson => sanitizeArgumentType(argJson)) + }); + + const allTypeChecks = createTypeChecks(sanitizedJson).reduce( + (carry, item) => + item instanceof Array ? [...carry, ...item] : [...carry, item], + [] + ); + const typeChecks = allTypeChecks.filter(check => typeof check === "object"); + const returnStatement = createReturnStatement(className, sanitizedJson); + return [...typeChecks, returnStatement]; +} + +function createTypeChecks(json) { + const checks = json.args.map(createTypeCheck); + checks.filter(check => Boolean(check)); + return checks; +} + +const callGlobal = sanitizerName => argIdentifier => + t.callExpression(t.identifier(sanitizerName), [t.identifier(argIdentifier)]); +const supportedContentSanitizersMap = { + GREYDirection: callGlobal("sanitize_greyDirection") +}; +function addArgumentContentSanitizerCall(json) { + if (supportedContentSanitizersMap[json.type]) { + return supportedContentSanitizersMap[json.type](json.name); + } + + return t.identifier(json.name); +} + +function createReturnStatement(className, json) { + const args = json.args.map(arg => + t.objectExpression([ + t.objectProperty(t.identifier("type"), t.stringLiteral(arg.type)), + t.objectProperty( + t.identifier("value"), + addArgumentContentSanitizerCall(arg) + ) + ]) + ); + + return t.returnStatement( + t.objectExpression([ + t.objectProperty( + t.identifier("target"), + t.objectExpression([ + t.objectProperty(t.identifier("type"), t.stringLiteral("Class")), + t.objectProperty(t.identifier("value"), t.stringLiteral(className)) + ]) + ), + t.objectProperty(t.identifier("method"), t.stringLiteral(json.name)), + t.objectProperty(t.identifier("args"), t.arrayExpression(args)) + ]) + ); +} + +function createTypeCheck(json) { + const typeInterfaces = { + NSInteger: isNumber, + CGFloat: isNumber, + CGPoint: isPoint, + CFTimeInterval: isNumber, + double: isNumber, + float: isNumber, + NSString: isString, + BOOL: isBoolean, + "NSDate *": isNumber, + GREYDirection: isOneOf(["left", "right", "up", "down"]), + GREYContentEdge: isOneOf(["left", "right", "top", "bottom"]), + GREYPinchDirection: isOneOf(["outward", "inward"]) + }; + + const typeCheckCreator = typeInterfaces[json.type]; + const isListOfChecks = typeCheckCreator instanceof Array; + + if (typeof typeCheckCreator !== "function" && !isListOfChecks) { + console.info("Could not find ", json); + return; + } + + return isListOfChecks + ? typeCheckCreator.map(singleCheck => singleCheck(json)) + : typeCheckCreator(json); +} + +module.exports = function(files) { + Object.entries(files).forEach(([inputFile, outputFile]) => { + const input = fs.readFileSync(inputFile, "utf8"); + + const json = objectiveCParser(input); + const ast = t.program([createClass(json), createExport(json)]); + const output = generate(ast); + + const commentBefore = "/**\n\n\tThis code is generated.\n\tFor more information see generation/README.md.\n*/\n\n"; + + // Add global helper functions + const globalFunctionsSource = fs.readFileSync(__dirname + "/global-functions.js", "utf8"); + const globalFunctions = globalFunctionsSource.substr(0, globalFunctionsSource.indexOf("module.exports")); + + const code = [commentBefore, globalFunctions, output.code].join('\n'); + fs.writeFileSync(outputFile, code, "utf8"); + }); +}; diff --git a/generation/fixtures/example.h b/generation/fixtures/example.h new file mode 100644 index 0000000000..7b16fbf270 --- /dev/null +++ b/generation/fixtures/example.h @@ -0,0 +1,55 @@ + +#import +#import +#import + +@protocol GREYAction; + +/** + * A interface that exposes UI element actions. + */ +@interface GREYActions : NSObject + +// Single Line Comment here ++ (id)actionForMultipleTapsWithCount:(NSUInteger)count; + +/** + * Multi Line Comment here + * Awesome + */ ++ (id)actionForMultipleTapsWithCount:(NSUInteger)count atPoint:(CGPoint)point; + +/** + * Returns a scroll action that scrolls in a @c direction for an @c amount of points starting from + * the given start point specified as percentages. @c xOriginStartPercentage is the x start + * position as a percentage of the total width of the scrollable visible area, + * @c yOriginStartPercentage is the y start position as a percentage of the total height of the + * scrollable visible area. @c xOriginStartPercentage and @c yOriginStartPercentage must be between + * 0 and 1, exclusive. + * + * @param direction The direction of the scroll. + * @param amount The amount scroll in points to inject. + * @param xOriginStartPercentage X coordinate of the start point specified as a percentage (0, 1) + * exclusive, of the total width of the scrollable visible area. + * @param yOriginStartPercentage Y coordinate of the start point specified as a percentage (0, 1) + * exclusive, of the total height of the scrollable visible area. + * + * @return A GREYAction that scrolls a scroll view in a given @c direction for a given @c amount + * starting from the given start points. + */ ++ (id)actionForScrollInDirection:(GREYDirection)direction + amount:(CGFloat)amount + xOriginStartPercentage:(CGFloat)xOriginStartPercentage + yOriginStartPercentage:(CGFloat)yOriginStartPercentage; + +/** + * Returns an action that uses the iOS keyboard to input a string. + * + * @param text The text to be typed. For Objective-C, backspace is supported by using "\b" in the + * string and "\u{8}" in Swift strings. Return key is supported with "\n". + * For Example: @"Helpo\b\bloWorld" will type HelloWorld in Objective-C. + * "Helpo\u{8}\u{8}loWorld" will type HelloWorld in Swift. + * + * @return A GREYAction to type a specific text string in a text field. + */ ++ (id)actionForTypeText:(NSString *)text; \ No newline at end of file diff --git a/generation/helpers.js b/generation/helpers.js new file mode 100644 index 0000000000..d7f3262641 --- /dev/null +++ b/generation/helpers.js @@ -0,0 +1,15 @@ +function capitalizeFirstLetter(string) { + return string.charAt(0).toUpperCase() + string.slice(1); +} + +function methodNameToSnakeCase(name) { + return name + .split(':') + .map((item, index) => + index === 0 ? item : capitalizeFirstLetter(item) + ).join(''); +} + +module.exports = { + methodNameToSnakeCase, +}; \ No newline at end of file diff --git a/generation/index.js b/generation/index.js new file mode 100755 index 0000000000..404627dd50 --- /dev/null +++ b/generation/index.js @@ -0,0 +1,7 @@ +#!/usr/bin/env node +const generateEarlGreyAdapters = require("./earl-grey"); +const files = { + "../detox/ios/EarlGrey/EarlGrey/Action/GREYActions.h": "../detox/src/ios/earlgreyapi/GREYActions.js" +}; + +generateEarlGreyAdapters(files); diff --git a/generation/package.json b/generation/package.json new file mode 100644 index 0000000000..89b3dc777b --- /dev/null +++ b/generation/package.json @@ -0,0 +1,39 @@ +{ + "name": "generation", + "version": "0.0.1", + "description": "Generate wrapper code for native dependencies", + "main": "index.js", + "private": true, + "scripts": { + "build": "./index.js", + "test": "jest" + }, + "author": "DanielMSchmidt ", + "license": "MIT", + "devDependencies": { + "babel-generator": "^6.25.0", + "babel-types": "^6.25.0", + "jest": "^20.0.4", + "lerna": "2.0.0-rc.4", + "objective-c-parser": "1.0.4", + "remove": "^0.1.5" + }, + "jest": { + "coveragePathIgnorePatterns": [ + "/index.js" + ], + "resetMocks": true, + "resetModules": true, + "coverageThreshold": { + "global": { + "statements": 100, + "branches": 100, + "functions": 100, + "lines": 100 + } + } + }, + "dependencies": { + "babel-generate-guard-clauses": "^0.1.0" + } +} diff --git a/lerna.json b/lerna.json index 61993a488c..46dfe5d615 100644 --- a/lerna.json +++ b/lerna.json @@ -7,7 +7,8 @@ "examples/demo-native-android", "examples/demo-native-ios", "examples/demo-react-native", - "detox/test" + "detox/test", + "generation" ], "commands": { "publish": { @@ -15,7 +16,8 @@ "detox-demo-native-android", "detox-demo-native-ios", "detox-demo-react-native", - "detox-test" + "detox-test", + "generation" ] } }, diff --git a/package.json b/package.json index cdd077bb70..f34025063a 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,9 @@ { - "devDependencies": { - "lerna": "2.0.0-rc.4" - }, "scripts": { - "release":"node scripts/release.js", - "e2e": "pushd detox/test && npm run e2e && popd" + "release": "node scripts/release.js", + "test": "npm run test:lerna && npm run test:e2e && npm run test:generation", + "test:lerna": "lerna run --ignore detox-demo* build && lerna run --ignore detox-demo* test", + "test:e2e": "pushd detox/test && npm run e2e && popd", + "test:generation": "pushd generation && npm run test && popd" } } diff --git a/scripts/ci.sh b/scripts/ci.sh index d7b4686421..2ca7c6ce0f 100755 --- a/scripts/ci.sh +++ b/scripts/ci.sh @@ -1,11 +1,9 @@ #!/bin/bash -e lerna bootstrap -lerna run --ignore detox-demo* build -lerna run --ignore detox-demo* test set -o pipefail && xcodebuild -project detox/ios/Detox.xcodeproj -scheme Detox -configuration Debug -sdk iphonesimulator build-for-testing | xcpretty set -o pipefail && xcodebuild -project detox/ios/Detox.xcodeproj -scheme Detox -configuration Debug -sdk iphonesimulator test-without-building -destination 'platform=iOS Simulator,name=iPhone 7 Plus' | xcpretty -npm run e2e +npm test #npm run release \ No newline at end of file