From dd32eb3de5c0f18c0a9118cfffe0c47cde0c1ee3 Mon Sep 17 00:00:00 2001 From: Daniel Schmidt Date: Wed, 4 Oct 2017 11:56:37 +0200 Subject: [PATCH] Move matchers to generated code (#306) * generation: add tests for helper methods * generation: remove unused code part * generation: add transformed grey matchers * generation: remove prompt if there is no filtered method * generation: replace generated matchers * generation: generate GREYMatcher adapter code * generation: refactor hand written adapters to generated ones * generation: add support for Matcher ids This allows us to transform the rest of the matchers and it is a potential solution for other transformations regarding ids as well --- .../src/ios/earlgreyapi/GREYMatchers+Detox.js | 179 ++++++ detox/src/ios/earlgreyapi/GREYMatchers.js | 577 ++++++++++++++++++ detox/src/ios/matchers.js | 37 +- .../__tests__/__snapshots__/earl-grey.js.snap | 6 + .../__tests__/__snapshots__/helpers.js.snap | 3 + generation/__tests__/earl-grey.js | 156 +++++ generation/__tests__/helpers.js | 13 + generation/earl-grey/index.js | 45 +- generation/fixtures/example.h | 3 +- generation/index.js | 4 +- generation/package.json | 7 +- 11 files changed, 991 insertions(+), 39 deletions(-) create mode 100644 detox/src/ios/earlgreyapi/GREYMatchers+Detox.js create mode 100644 detox/src/ios/earlgreyapi/GREYMatchers.js create mode 100644 generation/__tests__/__snapshots__/helpers.js.snap create mode 100644 generation/__tests__/helpers.js diff --git a/detox/src/ios/earlgreyapi/GREYMatchers+Detox.js b/detox/src/ios/earlgreyapi/GREYMatchers+Detox.js new file mode 100644 index 0000000000..9bb25fc868 --- /dev/null +++ b/detox/src/ios/earlgreyapi/GREYMatchers+Detox.js @@ -0,0 +1,179 @@ +/** + + 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}`); + } +} + +function sanitize_greyContentEdge(action) { + switch (action) { + case "left": + return 0; + case "right": + return 1; + case "top": + return 2; + case "bottom": + return 3; + + default: + throw new Error(`GREYAction.GREYContentEdge must be a 'left'/'right'/'top'/'bottom', got ${action}`); + } +} + + + +class GREYMatchers { + static detoxMatcherForText(text) { + if (typeof text !== "string") throw new Error("text should be a string, but got " + (text + (" (" + (typeof text + ")")))); + return { + target: { + type: "Class", + value: "GREYMatchers" + }, + method: "detoxMatcherForText:", + args: [{ + type: "NSString", + value: text + }] + }; + } + + static detoxMatcherForScrollChildOfMatcher(matcher) { + if (typeof matcher !== "object" || matcher.type !== "Invocation" || typeof matcher.value !== "object" || typeof matcher.value.target !== "object" || matcher.value.target.value !== "GREYMatchers") { + throw new Error('matcher should be a GREYMatcher, but got ' + JSON.stringify(matcher)); + } + + return { + target: { + type: "Class", + value: "GREYMatchers" + }, + method: "detoxMatcherForScrollChildOfMatcher:", + args: [matcher] + }; + } + + static detoxMatcherAvoidingProblematicReactNativeElements(matcher) { + if (typeof matcher !== "object" || matcher.type !== "Invocation" || typeof matcher.value !== "object" || typeof matcher.value.target !== "object" || matcher.value.target.value !== "GREYMatchers") { + throw new Error('matcher should be a GREYMatcher, but got ' + JSON.stringify(matcher)); + } + + return { + target: { + type: "Class", + value: "GREYMatchers" + }, + method: "detoxMatcherAvoidingProblematicReactNativeElements:", + args: [matcher] + }; + } + + static detoxMatcherForBothAnd(firstMatcher, secondMatcher) { + if (typeof firstMatcher !== "object" || firstMatcher.type !== "Invocation" || typeof firstMatcher.value !== "object" || typeof firstMatcher.value.target !== "object" || firstMatcher.value.target.value !== "GREYMatchers") { + throw new Error('firstMatcher should be a GREYMatcher, but got ' + JSON.stringify(firstMatcher)); + } + + if (typeof secondMatcher !== "object" || secondMatcher.type !== "Invocation" || typeof secondMatcher.value !== "object" || typeof secondMatcher.value.target !== "object" || secondMatcher.value.target.value !== "GREYMatchers") { + throw new Error('secondMatcher should be a GREYMatcher, but got ' + JSON.stringify(secondMatcher)); + } + + return { + target: { + type: "Class", + value: "GREYMatchers" + }, + method: "detoxMatcherForBoth:and:", + args: [firstMatcher, secondMatcher] + }; + } + + static detoxMatcherForBothAndAncestorMatcher(firstMatcher, ancestorMatcher) { + if (typeof firstMatcher !== "object" || firstMatcher.type !== "Invocation" || typeof firstMatcher.value !== "object" || typeof firstMatcher.value.target !== "object" || firstMatcher.value.target.value !== "GREYMatchers") { + throw new Error('firstMatcher should be a GREYMatcher, but got ' + JSON.stringify(firstMatcher)); + } + + if (typeof ancestorMatcher !== "object" || ancestorMatcher.type !== "Invocation" || typeof ancestorMatcher.value !== "object" || typeof ancestorMatcher.value.target !== "object" || ancestorMatcher.value.target.value !== "GREYMatchers") { + throw new Error('ancestorMatcher should be a GREYMatcher, but got ' + JSON.stringify(ancestorMatcher)); + } + + return { + target: { + type: "Class", + value: "GREYMatchers" + }, + method: "detoxMatcherForBoth:andAncestorMatcher:", + args: [firstMatcher, ancestorMatcher] + }; + } + + static detoxMatcherForBothAndDescendantMatcher(firstMatcher, descendantMatcher) { + if (typeof firstMatcher !== "object" || firstMatcher.type !== "Invocation" || typeof firstMatcher.value !== "object" || typeof firstMatcher.value.target !== "object" || firstMatcher.value.target.value !== "GREYMatchers") { + throw new Error('firstMatcher should be a GREYMatcher, but got ' + JSON.stringify(firstMatcher)); + } + + if (typeof descendantMatcher !== "object" || descendantMatcher.type !== "Invocation" || typeof descendantMatcher.value !== "object" || typeof descendantMatcher.value.target !== "object" || descendantMatcher.value.target.value !== "GREYMatchers") { + throw new Error('descendantMatcher should be a GREYMatcher, but got ' + JSON.stringify(descendantMatcher)); + } + + return { + target: { + type: "Class", + value: "GREYMatchers" + }, + method: "detoxMatcherForBoth:andDescendantMatcher:", + args: [firstMatcher, descendantMatcher] + }; + } + + static detoxMatcherForNot(matcher) { + if (typeof matcher !== "object" || matcher.type !== "Invocation" || typeof matcher.value !== "object" || typeof matcher.value.target !== "object" || matcher.value.target.value !== "GREYMatchers") { + throw new Error('matcher should be a GREYMatcher, but got ' + JSON.stringify(matcher)); + } + + return { + target: { + type: "Class", + value: "GREYMatchers" + }, + method: "detoxMatcherForNot:", + args: [matcher] + }; + } + + static detoxMatcherForClass(aClassName) { + if (typeof aClassName !== "string") throw new Error("aClassName should be a string, but got " + (aClassName + (" (" + (typeof aClassName + ")")))); + return { + target: { + type: "Class", + value: "GREYMatchers" + }, + method: "detoxMatcherForClass:", + args: [{ + type: "NSString", + value: aClassName + }] + }; + } + +} + +module.exports = GREYMatchers; \ No newline at end of file diff --git a/detox/src/ios/earlgreyapi/GREYMatchers.js b/detox/src/ios/earlgreyapi/GREYMatchers.js new file mode 100644 index 0000000000..17dbebb7ef --- /dev/null +++ b/detox/src/ios/earlgreyapi/GREYMatchers.js @@ -0,0 +1,577 @@ +/** + + 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}`); + } +} + +function sanitize_greyContentEdge(action) { + switch (action) { + case "left": + return 0; + case "right": + return 1; + case "top": + return 2; + case "bottom": + return 3; + + default: + throw new Error(`GREYAction.GREYContentEdge must be a 'left'/'right'/'top'/'bottom', got ${action}`); + } +} + + + +class GREYMatchers { + /*Matcher for application's key window. + +@return A matcher for the application's key window. +*/static matcherForKeyWindow() { + return { + target: { + type: "Class", + value: "GREYMatchers" + }, + method: "matcherForKeyWindow", + args: [] + }; + } + + /*Matcher for UI element with the provided accessibility @c label. + +@param label The accessibility label to be matched. + +@return A matcher for the accessibility label of an accessible element. +*/static matcherForAccessibilityLabel(label) { + if (typeof label !== "string") throw new Error("label should be a string, but got " + (label + (" (" + (typeof label + ")")))); + return { + target: { + type: "Class", + value: "GREYMatchers" + }, + method: "matcherForAccessibilityLabel:", + args: [{ + type: "NSString", + value: label + }] + }; + } + + /*Matcher for UI element with the provided accessibility ID @c accessibilityID. + +@param accessibilityID The accessibility ID to be matched. + +@return A matcher for the accessibility ID of an accessible element. +*/static matcherForAccessibilityID(accessibilityID) { + if (typeof accessibilityID !== "string") throw new Error("accessibilityID should be a string, but got " + (accessibilityID + (" (" + (typeof accessibilityID + ")")))); + return { + target: { + type: "Class", + value: "GREYMatchers" + }, + method: "matcherForAccessibilityID:", + args: [{ + type: "NSString", + value: accessibilityID + }] + }; + } + + /*Matcher for UI element with the provided accessibility @c value. + +@param value The accessibility value to be matched. + +@return A matcher for the accessibility value of an accessible element. +*/static matcherForAccessibilityValue(value) { + if (typeof value !== "string") throw new Error("value should be a string, but got " + (value + (" (" + (typeof value + ")")))); + return { + target: { + type: "Class", + value: "GREYMatchers" + }, + method: "matcherForAccessibilityValue:", + args: [{ + type: "NSString", + value: value + }] + }; + } + + /*Matcher for UI element with the provided accessiblity @c hint. + +@param hint The accessibility hint to be matched. + +@return A matcher for the accessibility hint of an accessible element. +*/static matcherForAccessibilityHint(hint) { + if (typeof hint !== "string") throw new Error("hint should be a string, but got " + (hint + (" (" + (typeof hint + ")")))); + return { + target: { + type: "Class", + value: "GREYMatchers" + }, + method: "matcherForAccessibilityHint:", + args: [{ + type: "NSString", + value: hint + }] + }; + } + + /*Matcher for UI element with accessiblity focus. + +@return A matcher for the accessibility focus of an accessible element. +*/static matcherForAccessibilityElementIsFocused() { + return { + target: { + type: "Class", + value: "GREYMatchers" + }, + method: "matcherForAccessibilityElementIsFocused", + args: [] + }; + } + + /*Matcher for UI elements of type UIButton, UILabel, UITextField or UITextView displaying the +provided @c inputText. + +@param text The text to be matched in the UI elements. + +@return A matcher to check for any UI elements with a text field containing the given text. +*/static matcherForText(text) { + if (typeof text !== "string") throw new Error("text should be a string, but got " + (text + (" (" + (typeof text + ")")))); + return { + target: { + type: "Class", + value: "GREYMatchers" + }, + method: "matcherForText:", + args: [{ + type: "NSString", + value: text + }] + }; + } + + /*Matcher for element that is the first responder. + +@return A matcher that verifies if a UI element is the first responder. +*/static matcherForFirstResponder() { + return { + target: { + type: "Class", + value: "GREYMatchers" + }, + method: "matcherForFirstResponder", + args: [] + }; + } + + /*Matcher to check if system alert view is shown. + +@return A matcher to check if a system alert view is being shown. +*/static matcherForSystemAlertViewShown() { + return { + target: { + type: "Class", + value: "GREYMatchers" + }, + method: "matcherForSystemAlertViewShown", + args: [] + }; + } + + /*Matcher for UI element whose percent visible area (of its accessibility frame) exceeds the +given @c percent. + +@param percent The percent visible area that the UI element being matched has to be visible. +Allowed values for @c percent are [0,1] inclusive. + +@return A matcher that checks if a UI element has a visible area at least equal +to a minimum value. +*/static matcherForMinimumVisiblePercent(percent) { + if (typeof percent !== "number") throw new Error("percent should be a number, but got " + (percent + (" (" + (typeof percent + ")")))); + return { + target: { + type: "Class", + value: "GREYMatchers" + }, + method: "matcherForMinimumVisiblePercent:", + args: [{ + type: "CGFloat", + value: percent + }] + }; + } + + /*Matcher for UI element that is sufficiently visible to the user. EarlGrey considers elements +with visible area percentage greater than @c kElementSufficientlyVisiblePercentage (0.75) +to be sufficiently visible. + +@return A matcher intialized with a visibility percentage that confirms an element is +sufficiently visible. +*/static matcherForSufficientlyVisible() { + return { + target: { + type: "Class", + value: "GREYMatchers" + }, + method: "matcherForSufficientlyVisible", + args: [] + }; + } + + /*Matcher for UI element that are not visible to the user i.e. has a zero visible area. + +@return A matcher for verifying if an element is not visible. +*/static matcherForNotVisible() { + return { + target: { + type: "Class", + value: "GREYMatchers" + }, + method: "matcherForNotVisible", + args: [] + }; + } + + /*Matcher for UI element that matches EarlGrey's criteria for user interaction currently it must +satisfy at least the following criteria: + + +@return A matcher that checks if a UI element is interactable. +*/static matcherForInteractable() { + return { + target: { + type: "Class", + value: "GREYMatchers" + }, + method: "matcherForInteractable", + args: [] + }; + } + + /*Matcher to check if a UI element is accessible. + +@return A matcher that checks if a UI element is accessible. +*/static matcherForAccessibilityElement() { + return { + target: { + type: "Class", + value: "GREYMatchers" + }, + method: "matcherForAccessibilityElement", + args: [] + }; + } + + /*Matcher for matching UIProgressView's values. Use greaterThan, greaterThanOrEqualTo, +lessThan etc to create @c comparisonMatcher. For example, to match the UIProgressView +elements that have progress value greater than 50.2, use +@code [GREYMatchers matcherForProgress:grey_greaterThan(@(50.2))] @endcode. In case if an +unimplemented matcher is required, please implement it similar to @c grey_closeTo. + +@param comparisonMatcher The matcher with the value to check the progress against. + +@return A matcher for checking a UIProgessView's value. +*/static matcherForProgress(comparisonMatcher) { + if (typeof comparisonMatcher !== "object" || comparisonMatcher.type !== "Invocation" || typeof comparisonMatcher.value !== "object" || typeof comparisonMatcher.value.target !== "object" || comparisonMatcher.value.target.value !== "GREYMatchers") { + throw new Error('comparisonMatcher should be a GREYMatcher, but got ' + JSON.stringify(comparisonMatcher)); + } + + return { + target: { + type: "Class", + value: "GREYMatchers" + }, + method: "matcherForProgress:", + args: [comparisonMatcher] + }; + } + + /*Matcher that matches UI element based on the presence of an ancestor in its hierarchy. +The given matcher is used to match decendants. + +@param ancestorMatcher The ancestor UI element whose descendants are to be matched. + +@return A matcher to check if a UI element is the descendant of another. +*/static matcherForAncestor(ancestorMatcher) { + if (typeof ancestorMatcher !== "object" || ancestorMatcher.type !== "Invocation" || typeof ancestorMatcher.value !== "object" || typeof ancestorMatcher.value.target !== "object" || ancestorMatcher.value.target.value !== "GREYMatchers") { + throw new Error('ancestorMatcher should be a GREYMatcher, but got ' + JSON.stringify(ancestorMatcher)); + } + + return { + target: { + type: "Class", + value: "GREYMatchers" + }, + method: "matcherForAncestor:", + args: [ancestorMatcher] + }; + } + + /*Matcher that matches any UI element with a descendant matching the given matcher. + +@param descendantMatcher A matcher being checked to be a descendant +of the UI element being checked. + +@return A matcher to check if a the specified element is in a descendant of another UI element. +*/static matcherForDescendant(descendantMatcher) { + if (typeof descendantMatcher !== "object" || descendantMatcher.type !== "Invocation" || typeof descendantMatcher.value !== "object" || typeof descendantMatcher.value.target !== "object" || descendantMatcher.value.target.value !== "GREYMatchers") { + throw new Error('descendantMatcher should be a GREYMatcher, but got ' + JSON.stringify(descendantMatcher)); + } + + return { + target: { + type: "Class", + value: "GREYMatchers" + }, + method: "matcherForDescendant:", + args: [descendantMatcher] + }; + } + + /*Matcher that matches UIButton that has title label as @c text. + +@param title The title to be checked on the UIButtons being matched. + +@return A matcher to confirm UIButton titles. +*/static matcherForButtonTitle(title) { + if (typeof title !== "string") throw new Error("title should be a string, but got " + (title + (" (" + (typeof title + ")")))); + return { + target: { + type: "Class", + value: "GREYMatchers" + }, + method: "matcherForButtonTitle:", + args: [{ + type: "NSString", + value: title + }] + }; + } + + /*Matcher that matches UIScrollView that has contentOffset as @c offset. + +@param offset The content offset to be checked on the UIScrollView being +matched. + +@return A matcher to confirm UIScrollView content offset. +*/static matcherForScrollViewContentOffset(offset) { + if (typeof offset !== "object") throw new Error("offset should be a object, but got " + (offset + (" (" + (typeof offset + ")")))); + if (typeof offset.x !== "number") throw new Error("offset.x should be a number, but got " + (offset.x + (" (" + (typeof offset.x + ")")))); + if (typeof offset.y !== "number") throw new Error("offset.y should be a number, but got " + (offset.y + (" (" + (typeof offset.y + ")")))); + return { + target: { + type: "Class", + value: "GREYMatchers" + }, + method: "matcherForScrollViewContentOffset:", + args: [{ + type: "CGPoint", + value: offset + }] + }; + } + + /*Matcher that matches a UISlider's value. + +@param valueMatcher A matcher for the UISlider's value. You must provide a valid +@c valueMatcher for the floating point value comparison. The +@c valueMatcher should be of the type @c closeTo, @c greaterThan, +@c lessThan, @c lessThanOrEqualTo, @c greaterThanOrEqualTo. The +value matchers should account for any loss in precision for the given +floating point value. If you are using @c grey_closeTo, use delta diff as +@c kGREYAcceptableFloatDifference. In case if an unimplemented matcher +is required, please implement it similar to @c grey_closeTo. + +@return A matcher for checking a UISlider's value. +*/static matcherForSliderValueMatcher(valueMatcher) { + if (typeof valueMatcher !== "object" || valueMatcher.type !== "Invocation" || typeof valueMatcher.value !== "object" || typeof valueMatcher.value.target !== "object" || valueMatcher.value.target.value !== "GREYMatchers") { + throw new Error('valueMatcher should be a GREYMatcher, but got ' + JSON.stringify(valueMatcher)); + } + + return { + target: { + type: "Class", + value: "GREYMatchers" + }, + method: "matcherForSliderValueMatcher:", + args: [valueMatcher] + }; + } + + /*Matcher that matches UIPickerView that has a column set to @c value. + +@param column The column of the UIPickerView to be matched. +@param value The value that should be set in the column of the UIPickerView. + +@return A matcher to check the value in a particular column of a UIPickerView. +*/static matcherForPickerColumnSetToValue(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: "GREYMatchers" + }, + method: "matcherForPickerColumn:setToValue:", + args: [{ + type: "NSInteger", + value: column + }, { + type: "NSString", + value: value + }] + }; + } + + /*Matcher that verifies whether an element, that is a UIControl, is enabled. + +@return A matcher for checking whether a UI element is an enabled UIControl. +*/static matcherForEnabledElement() { + return { + target: { + type: "Class", + value: "GREYMatchers" + }, + method: "matcherForEnabledElement", + args: [] + }; + } + + /*Matcher that verifies whether an element, that is a UIControl, is selected. + +@return A matcher for checking whether a UI element is a selected UIControl. +*/static matcherForSelectedElement() { + return { + target: { + type: "Class", + value: "GREYMatchers" + }, + method: "matcherForSelectedElement", + args: [] + }; + } + + /*Matcher that verifies whether a view has its userInteractionEnabled property set to @c YES. + +@return A matcher for checking whether a view' userInteractionEnabled property is set to @c YES. +*/static matcherForUserInteractionEnabled() { + return { + target: { + type: "Class", + value: "GREYMatchers" + }, + method: "matcherForUserInteractionEnabled", + args: [] + }; + } + + /*Matcher primarily for asserting that the element is @c nil or not found. + +@return A matcher to check if a specified element is @c nil or not found. +*/static matcherForNil() { + return { + target: { + type: "Class", + value: "GREYMatchers" + }, + method: "matcherForNil", + args: [] + }; + } + + /*Matcher for asserting that the element exists in the UI hierarchy (i.e. not @c nil). + +@return A matcher to check if a specified element is not @c nil. +*/static matcherForNotNil() { + return { + target: { + type: "Class", + value: "GREYMatchers" + }, + method: "matcherForNotNil", + args: [] + }; + } + + /*A Matcher that matches against any object, including @c nils. + +@return A matcher that matches any object. +*/static matcherForAnything() { + return { + target: { + type: "Class", + value: "GREYMatchers" + }, + method: "matcherForAnything", + args: [] + }; + } + + /*Matcher that matches a UIScrollView scrolled to content @c edge. + +@param edge The content edge UIScrollView should be scrolled to. + +@return A matcher that matches a UIScrollView scrolled to content @c edge. +*/static matcherForScrolledToContentEdge(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: "GREYMatchers" + }, + method: "matcherForScrolledToContentEdge:", + args: [{ + type: "NSInteger", + value: sanitize_greyContentEdge(edge) + }] + }; + } + + /*Matcher that matches a UITextField's content. + +@param value The text string contained inside the UITextField. + +@return A matcher that matches the value inside a UITextField. +*/static matcherForTextFieldValue(value) { + if (typeof value !== "string") throw new Error("value should be a string, but got " + (value + (" (" + (typeof value + ")")))); + return { + target: { + type: "Class", + value: "GREYMatchers" + }, + method: "matcherForTextFieldValue:", + args: [{ + type: "NSString", + value: value + }] + }; + } + +} + +module.exports = GREYMatchers; \ No newline at end of file diff --git a/detox/src/ios/matchers.js b/detox/src/ios/matchers.js index badfc113f6..16753d52ef 100644 --- a/detox/src/ios/matchers.js +++ b/detox/src/ios/matchers.js @@ -1,37 +1,36 @@ const invoke = require('../invoke'); +const GreyMatchers = require('./earlgreyapi/GREYMatchers'); +const GreyMatchersDetox = require('./earlgreyapi/GREYMatchers+Detox'); class Matcher { withAncestor(matcher) { - if (!(matcher instanceof Matcher)) throw new Error(`Matcher withAncestor argument must be a valid Matcher, got ${typeof matcher}`); const _originalMatcherCall = this._call; - this._call = invoke.call(invoke.IOS.Class('GREYMatchers'), 'detoxMatcherForBoth:andAncestorMatcher:', _originalMatcherCall, matcher._call); + this._call = invoke.callDirectly(GreyMatchersDetox.detoxMatcherForBothAndAncestorMatcher(_originalMatcherCall, matcher._call)); return this; } withDescendant(matcher) { - if (!(matcher instanceof Matcher)) throw new Error(`Matcher withDescendant argument must be a valid Matcher, got ${typeof matcher}`); const _originalMatcherCall = this._call; - this._call = invoke.call(invoke.IOS.Class('GREYMatchers'), 'detoxMatcherForBoth:andDescendantMatcher:', _originalMatcherCall, matcher._call); + this._call = invoke.callDirectly(GreyMatchersDetox.detoxMatcherForBothAndDescendantMatcher(_originalMatcherCall, matcher._call)); return this; } and(matcher) { - if (!(matcher instanceof Matcher)) throw new Error(`Matcher and argument must be a valid Matcher, got ${typeof matcher}`); const _originalMatcherCall = this._call; - this._call = invoke.call(invoke.IOS.Class('GREYMatchers'), 'detoxMatcherForBoth:and:', _originalMatcherCall, matcher._call); + this._call = invoke.callDirectly(GreyMatchersDetox.detoxMatcherForBothAnd(_originalMatcherCall, matcher._call)); return this; } not() { const _originalMatcherCall = this._call; - this._call = invoke.call(invoke.IOS.Class('GREYMatchers'), 'detoxMatcherForNot:', _originalMatcherCall); + this._call = invoke.callDirectly(GreyMatchersDetox.detoxMatcherForNot(_originalMatcherCall)); return this; } _avoidProblematicReactNativeElements() { const _originalMatcherCall = this._call; - this._call = invoke.call(invoke.IOS.Class('GREYMatchers'), 'detoxMatcherAvoidingProblematicReactNativeElements:', _originalMatcherCall); + this._call = invoke.callDirectly(GreyMatchersDetox.detoxMatcherAvoidingProblematicReactNativeElements(_originalMatcherCall)); return this; } _extendToDescendantScrollViews() { const _originalMatcherCall = this._call; - this._call = invoke.call(invoke.IOS.Class('GREYMatchers'), 'detoxMatcherForScrollChildOfMatcher:', _originalMatcherCall); + this._call = invoke.callDirectly(GreyMatchersDetox.detoxMatcherForScrollChildOfMatcher(_originalMatcherCall)); return this; } } @@ -47,16 +46,14 @@ class LabelMatcher extends Matcher { class IdMatcher extends Matcher { constructor(value) { super(); - if (typeof value !== 'string') throw new Error(`IdMatcher ctor argument must be a string, got ${typeof value}`); - this._call = invoke.call(invoke.IOS.Class('GREYMatchers'), 'matcherForAccessibilityID:', value); + this._call = invoke.callDirectly(GreyMatchers.matcherForAccessibilityID(value)); } } class TypeMatcher extends Matcher { constructor(value) { super(); - if (typeof value !== 'string') throw new Error(`TypeMatcher ctor argument must be a string, got ${typeof value}`); - this._call = invoke.call(invoke.IOS.Class('GREYMatchers'), 'detoxMatcherForClass:', value); + this._call = invoke.callDirectly(GreyMatchersDetox.detoxMatcherForClass(value)); } } @@ -93,44 +90,42 @@ class TraitsMatcher extends Matcher { class VisibleMatcher extends Matcher { constructor() { super(); - this._call = invoke.call(invoke.IOS.Class('GREYMatchers'), 'matcherForSufficientlyVisible'); + this._call = invoke.callDirectly(GreyMatchers.matcherForSufficientlyVisible()); } } class NotVisibleMatcher extends Matcher { constructor() { super(); - this._call = invoke.call(invoke.IOS.Class('GREYMatchers'), 'matcherForNotVisible'); + this._call = invoke.callDirectly(GreyMatchers.matcherForNotVisible()); } } class ExistsMatcher extends Matcher { constructor() { super(); - this._call = invoke.call(invoke.IOS.Class('GREYMatchers'), 'matcherForNotNil'); + this._call = invoke.callDirectly(GreyMatchers.matcherForNotNil()); } } class NotExistsMatcher extends Matcher { constructor() { super(); - this._call = invoke.call(invoke.IOS.Class('GREYMatchers'), 'matcherForNil'); + this._call = invoke.callDirectly(GreyMatchers.matcherForNil()); } } class TextMatcher extends Matcher { constructor(value) { super(); - if (typeof value !== 'string') throw new Error(`TextMatcher ctor argument must be a string, got ${typeof value}`); - this._call = invoke.call(invoke.IOS.Class('GREYMatchers'), 'detoxMatcherForText:', value); + this._call = invoke.callDirectly(GreyMatchersDetox.detoxMatcherForText(value)); } } class ValueMatcher extends Matcher { constructor(value) { super(); - if (typeof value !== 'string') throw new Error(`ValueMatcher ctor argument must be a string, got ${typeof value}`); - this._call = invoke.call(invoke.IOS.Class('GREYMatchers'), 'matcherForAccessibilityValue:', value); + this._call = invoke.callDirectly(GreyMatchers.matcherForAccessibilityValue(value)); } } diff --git a/generation/__tests__/__snapshots__/earl-grey.js.snap b/generation/__tests__/__snapshots__/earl-grey.js.snap index a44881bf56..b5f323fff1 100644 --- a/generation/__tests__/__snapshots__/earl-grey.js.snap +++ b/generation/__tests__/__snapshots__/earl-grey.js.snap @@ -92,3 +92,9 @@ Object { }, } `; + +exports[`earl-grey generation special case: id should fail with wrongly formatted matchers 1`] = `"firstMatcher should be a GREYMatcher, but got {\\"type\\":\\"Invocation\\",\\"value\\":{\\"target\\":{\\"type\\":\\"Class\\",\\"value\\":\\"GREYAction\\"},\\"method\\":\\"matcherForAccessibilityID:\\",\\"args\\":[\\"Grandfather883\\"]}}"`; + +exports[`earl-grey generation special case: id should fail with wrongly formatted matchers 2`] = `"ancestorMatcher should be a GREYMatcher, but got {\\"type\\":\\"Invocation\\",\\"value\\":{\\"target\\":{\\"type\\":\\"Class\\",\\"value\\":\\"GREYAction\\"},\\"method\\":\\"matcherForAccessibilityID:\\",\\"args\\":[\\"Grandson883\\"]}}"`; + +exports[`earl-grey generation special case: id should fail with wrongly formatted matchers 3`] = `"firstMatcher should be a GREYMatcher, but got {\\"type\\":\\"Invocation\\",\\"value\\":{\\"method\\":\\"matcherForAccessibilityID:\\",\\"args\\":[\\"Grandfather883\\"]}}"`; diff --git a/generation/__tests__/__snapshots__/helpers.js.snap b/generation/__tests__/__snapshots__/helpers.js.snap new file mode 100644 index 0000000000..41542647ab --- /dev/null +++ b/generation/__tests__/__snapshots__/helpers.js.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`helpers methodNameToSnakeCase should return the correct snake case method name 1`] = `"actionForScrollInDirectionAmountXOriginStartPercentageYOriginStartPercentage"`; diff --git a/generation/__tests__/earl-grey.js b/generation/__tests__/earl-grey.js index 7428516cd0..50f11fdbfe 100644 --- a/generation/__tests__/earl-grey.js +++ b/generation/__tests__/earl-grey.js @@ -166,6 +166,162 @@ describe("earl-grey generation", () => { }); }); + describe("special case: id", () => { + it("should not wrap the args in type id, but pass them in as they are", () => { + const ancestorMatcher = { + "type": "Invocation", + "value": { + "target": { + "type": "Class", + "value": "GREYMatchers" + }, + "method": "matcherForAccessibilityID:", + "args": [ + "Grandson883" + ] + } + }; + const currentMatcher = { + "type": "Invocation", + "value": { + "target": { + "type": "Class", + "value": "GREYMatchers" + }, + "method": "matcherForAccessibilityID:", + "args": [ + "Grandfather883" + ] + } + }; + const result = ExampleClass.detoxMatcherForBothAndAncestorMatcher(currentMatcher, ancestorMatcher); + + expect(result).toEqual({ + "target": { + "type": "Class", + "value": "GREYActions" + }, + "method": "detoxMatcherForBoth:andAncestorMatcher:", + "args": [ + { + "type": "Invocation", + "value": { + "target": { + "type": "Class", + "value": "GREYMatchers" + }, + "method": "matcherForAccessibilityID:", + "args": [ + "Grandfather883" + ] + } + }, + { + "type": "Invocation", + "value": { + "target": { + "type": "Class", + "value": "GREYMatchers" + }, + "method": "matcherForAccessibilityID:", + "args": [ + "Grandson883" + ] + } + } + ] + }); + }); + + it("should fail with wrongly formatted matchers", () => { + expect(() => { + const ancestorMatcher = { + "type": "Invocation", + "value": { + "target": { + "type": "Class", + "value": "GREYMatchers" + }, + "method": "matcherForAccessibilityID:", + "args": [ + "Grandson883" + ] + } + }; + const currentAction = { + "type": "Invocation", + "value": { + "target": { + "type": "Class", + "value": "GREYAction" + }, + "method": "matcherForAccessibilityID:", + "args": [ + "Grandfather883" + ] + } + }; + ExampleClass.detoxMatcherForBothAndAncestorMatcher(currentAction, ancestorMatcher); + }).toThrowErrorMatchingSnapshot(); + + expect(() => { + const ancestorAction = { + "type": "Invocation", + "value": { + "target": { + "type": "Class", + "value": "GREYAction" + }, + "method": "matcherForAccessibilityID:", + "args": [ + "Grandson883" + ] + } + }; + const currentMatcher = { + "type": "Invocation", + "value": { + "target": { + "type": "Class", + "value": "GREYMatchers" + }, + "method": "matcherForAccessibilityID:", + "args": [ + "Grandfather883" + ] + } + }; + ExampleClass.detoxMatcherForBothAndAncestorMatcher(currentMatcher, ancestorAction); + }).toThrowErrorMatchingSnapshot(); + + expect(() => { + const ancestorMatcher = { + "type": "Invocation", + "value": { + "target": { + "type": "Class", + "value": "GREYAction" + }, + "method": "matcherForAccessibilityID:", + "args": [ + "Grandson883" + ] + } + }; + const currentMatcher = { + "type": "Invocation", + "value": { + "method": "matcherForAccessibilityID:", + "args": [ + "Grandfather883" + ] + } + }; + ExampleClass.detoxMatcherForBothAndAncestorMatcher(currentMatcher, ancestorMatcher); + }).toThrowErrorMatchingSnapshot(); + }); + }); + afterAll(() => { // Clean up remove.removeSync("./__tests__/generated"); diff --git a/generation/__tests__/helpers.js b/generation/__tests__/helpers.js new file mode 100644 index 0000000000..04e5757829 --- /dev/null +++ b/generation/__tests__/helpers.js @@ -0,0 +1,13 @@ +const { methodNameToSnakeCase } = require('../helpers'); + +describe("helpers", () => { + describe("methodNameToSnakeCase", () => { + it("should not fail with empty string", () => { + expect(() => methodNameToSnakeCase("")).not.toThrow(); + }); + + it("should return the correct snake case method name", () => { + expect(methodNameToSnakeCase("actionForScrollInDirection:amount:xOriginStartPercentage:yOriginStartPercentage:")).toMatchSnapshot(); + }); + }); +}); diff --git a/generation/earl-grey/index.js b/generation/earl-grey/index.js index 5d31be4cfe..0f27be9e96 100644 --- a/generation/earl-grey/index.js +++ b/generation/earl-grey/index.js @@ -1,4 +1,5 @@ const t = require("babel-types"); +const template = require("babel-template"); const objectiveCParser = require("objective-c-parser"); const generate = require("babel-generator").default; const fs = require("fs"); @@ -19,6 +20,19 @@ const isPoint = [ generateTypeCheck("number", { selector: "y" }) ]; const isOneOf = generateIsOneOfCheck; +const isGreyMatcher = ({ name }) => template(` + if ( + typeof ARG !== "object" || + ARG.type !== "Invocation" || + typeof ARG.value !== "object" || + typeof ARG.value.target !== "object" || + ARG.value.target.value !== "GREYMatchers" + ) { + throw new Error('${name} should be a GREYMatcher, but got ' + JSON.stringify(ARG)); + } +`)({ + ARG: t.identifier(name) + }) // Constants const SUPPORTED_TYPES = [ @@ -30,6 +44,7 @@ const SUPPORTED_TYPES = [ "NSString *", "NSString", "NSUInteger", + "id" ]; /** @@ -172,8 +187,13 @@ function addArgumentTypeSanitizer(json) { return json.type; } +// These types need no wrapping with {type: ..., value: } +const plainArgumentTypes = ["id"]; +function shouldBeWrapped({ type }) { + return !plainArgumentTypes.includes(type); +} function createReturnStatement(className, json) { - const args = json.args.map(arg => + const args = json.args.map(arg => shouldBeWrapped(arg) ? t.objectExpression([ t.objectProperty( t.identifier("type"), @@ -183,7 +203,7 @@ function createReturnStatement(className, json) { t.identifier("value"), addArgumentContentSanitizerCall(arg) ) - ]) + ]) : addArgumentContentSanitizerCall(arg) ); return t.returnStatement( @@ -214,17 +234,13 @@ function createTypeCheck(json) { "NSDate *": isNumber, GREYDirection: isOneOf(["left", "right", "up", "down"]), GREYContentEdge: isOneOf(["left", "right", "top", "bottom"]), - GREYPinchDirection: isOneOf(["outward", "inward"]) + GREYPinchDirection: isOneOf(["outward", "inward"]), + "id": isGreyMatcher, }; 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); @@ -248,10 +264,13 @@ module.exports = function(files) { fs.writeFileSync(outputFile, code, "utf8"); // Output methods that were not created due to missing argument support - console.log(`Could not generate the following methods for ${json.name}`); - const unsupportedMethods = json.methods.filter(x => !filterMethodsWithUnsupportedParams(x)).forEach(method => { - const methodArgs = method.args.filter(methodArg => !SUPPORTED_TYPES.includes(methodArg.type)).map(methodArg => methodArg.type); - console.log(`\t ${method.name} misses ${methodArgs}`); - }); + const unsupportedMethods = json.methods.filter(x => !filterMethodsWithUnsupportedParams(x)); + if (unsupportedMethods.length) { + console.log(`Could not generate the following methods for ${json.name}`); + unsupportedMethods.forEach(method => { + const methodArgs = method.args.filter(methodArg => !SUPPORTED_TYPES.includes(methodArg.type)).map(methodArg => methodArg.type); + console.log(`\t ${method.name} misses ${methodArgs}`); + }); + } }); }; diff --git a/generation/fixtures/example.h b/generation/fixtures/example.h index 80b007678b..1ea0b904da 100644 --- a/generation/fixtures/example.h +++ b/generation/fixtures/example.h @@ -60,4 +60,5 @@ + (id)actionForScrollToContentEdge:(GREYContentEdge)edge; + (id)actionWithUnknownType:(WTFType *)wat; -+ (id)actionWithKnown:(NSUInteger)iknowdis andUnknownType:(WTFTypalike *)wat; \ No newline at end of file ++ (id)actionWithKnown:(NSUInteger)iknowdis andUnknownType:(WTFTypalike *)wat; ++ (id)detoxMatcherForBoth:(id)firstMatcher andAncestorMatcher:(id)ancestorMatcher; \ No newline at end of file diff --git a/generation/index.js b/generation/index.js index 404627dd50..16c8c46a61 100755 --- a/generation/index.js +++ b/generation/index.js @@ -1,7 +1,9 @@ #!/usr/bin/env node const generateEarlGreyAdapters = require("./earl-grey"); const files = { - "../detox/ios/EarlGrey/EarlGrey/Action/GREYActions.h": "../detox/src/ios/earlgreyapi/GREYActions.js" + "../detox/ios/EarlGrey/EarlGrey/Action/GREYActions.h": "../detox/src/ios/earlgreyapi/GREYActions.js", + "../detox/ios/Detox/GREYMatchers+Detox.h": "../detox/src/ios/earlgreyapi/GREYMatchers+Detox.js", + "../detox/ios/EarlGrey/EarlGrey/Matcher/GREYMatchers.h": "../detox/src/ios/earlgreyapi/GREYMatchers.js", }; generateEarlGreyAdapters(files); diff --git a/generation/package.json b/generation/package.json index 89b3dc777b..465365c106 100644 --- a/generation/package.json +++ b/generation/package.json @@ -15,7 +15,7 @@ "babel-types": "^6.25.0", "jest": "^20.0.4", "lerna": "2.0.0-rc.4", - "objective-c-parser": "1.0.4", + "objective-c-parser": "1.1.0", "remove": "^0.1.5" }, "jest": { @@ -34,6 +34,7 @@ } }, "dependencies": { - "babel-generate-guard-clauses": "^0.1.0" + "babel-generate-guard-clauses": "^2.0.0", + "babel-template": "^6.26.0" } -} +} \ No newline at end of file