From 0d7abb97ad5c138386f3b4eab5102e6b4348bba8 Mon Sep 17 00:00:00 2001 From: Daniel Schmidt Date: Tue, 3 Oct 2017 22:04:33 +0200 Subject: [PATCH] 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 | 102 ++++++++++++ detox/src/ios/earlgreyapi/GREYMatchers.js | 93 +++++++++++ detox/src/ios/matchers.js | 15 +- .../__tests__/__snapshots__/earl-grey.js.snap | 6 + generation/__tests__/earl-grey.js | 156 ++++++++++++++++++ generation/earl-grey/index.js | 27 ++- generation/fixtures/example.h | 3 +- generation/package.json | 5 +- 8 files changed, 392 insertions(+), 15 deletions(-) diff --git a/detox/src/ios/earlgreyapi/GREYMatchers+Detox.js b/detox/src/ios/earlgreyapi/GREYMatchers+Detox.js index 9f2a941cd4..9bb25fc868 100644 --- a/detox/src/ios/earlgreyapi/GREYMatchers+Detox.js +++ b/detox/src/ios/earlgreyapi/GREYMatchers+Detox.js @@ -57,6 +57,108 @@ class GREYMatchers { }; } + 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 { diff --git a/detox/src/ios/earlgreyapi/GREYMatchers.js b/detox/src/ios/earlgreyapi/GREYMatchers.js index d91a4a5d8c..17dbebb7ef 100644 --- a/detox/src/ios/earlgreyapi/GREYMatchers.js +++ b/detox/src/ios/earlgreyapi/GREYMatchers.js @@ -287,6 +287,72 @@ is visible. }; } + /*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. @@ -330,6 +396,33 @@ matched. }; } + /*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. diff --git a/detox/src/ios/matchers.js b/detox/src/ios/matchers.js index 3f4ffeb6e5..16753d52ef 100644 --- a/detox/src/ios/matchers.js +++ b/detox/src/ios/matchers.js @@ -4,36 +4,33 @@ 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; } } 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__/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/earl-grey/index.js b/generation/earl-grey/index.js index 8a9f1b0113..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,7 +234,8 @@ 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]; 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/package.json b/generation/package.json index ae0679db9c..465365c106 100644 --- a/generation/package.json +++ b/generation/package.json @@ -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