diff --git a/detox/src/devices/IosDriver.js b/detox/src/devices/IosDriver.js index a16652c1d3..75fe6ce9fc 100644 --- a/detox/src/devices/IosDriver.js +++ b/detox/src/devices/IosDriver.js @@ -5,6 +5,7 @@ const InvocationManager = require('../invoke').InvocationManager; const invoke = require('../invoke'); const GREYConfigurationApi = require('./../ios/earlgreyapi/GREYConfiguration'); const GREYConfigurationDetox = require('./../ios/earlgreyapi/GREYConfigurationDetox'); +const EarlyGrey = require('./../ios/earlgreyapi/EarlGrey'); class IosDriver extends DeviceDriverBase { constructor(client) { @@ -27,7 +28,7 @@ class IosDriver extends DeviceDriverBase { async setURLBlacklist(urlList) { await this.client.execute( GREYConfigurationApi.setValueForConfigKey( - GREYConfigurationApi.sharedInstance(), + invoke.callDirectly(GREYConfigurationApi.sharedInstance()), urlList, "GREYConfigKeyURLBlacklistRegex" ) @@ -37,7 +38,7 @@ class IosDriver extends DeviceDriverBase { async enableSynchronization() { await this.client.execute( GREYConfigurationDetox.enableSynchronization( - GREYConfigurationApi.sharedInstance() + invoke.callDirectly(GREYConfigurationApi.sharedInstance()) ) ); } @@ -45,7 +46,7 @@ class IosDriver extends DeviceDriverBase { async disableSynchronization() { await this.client.execute( GREYConfigurationDetox.disableSynchronization( - GREYConfigurationApi.sharedInstance() + invoke.callDirectly(GREYConfigurationApi.sharedInstance()) ) ); } @@ -55,19 +56,8 @@ class IosDriver extends DeviceDriverBase { } async setOrientation(deviceId, orientation) { - // keys are possible orientations - const orientationMapping = { - landscape: 3, // top at left side landscape - portrait: 1 // non-reversed portrait - }; - if (!Object.keys(orientationMapping).includes(orientation)) { - throw new Error(`setOrientation failed: provided orientation ${orientation} is not part of supported orientations: ${Object.keys(orientationMapping)}`); - } + const call = EarlyGrey.rotateDeviceToOrientationErrorOrNil(invoke.EarlGrey.instance,orientation); - const call = invoke.call(invoke.EarlGrey.instance, - 'rotateDeviceToOrientation:errorOrNil:', - invoke.IOS.NSInteger(orientationMapping[orientation]) - ); await this.client.execute(call); } diff --git a/detox/src/ios/earlgreyapi/EarlGrey.js b/detox/src/ios/earlgreyapi/EarlGrey.js new file mode 100644 index 0000000000..217f3c917e --- /dev/null +++ b/detox/src/ios/earlgreyapi/EarlGrey.js @@ -0,0 +1,83 @@ +/** + + This code is generated. + For more information see generation/README.md. +*/ + + +function sanitize_uiDeviceOrientation(value) { + const orientationMapping = { + landscape: 3, // top at left side landscape + portrait: 1 // non-reversed portrait + }; + + return orientationMapping[value]; +} +class EarlGreyImpl { + /*Provides the file name and line number of the code that is calling into EarlGrey. +In case of a failure, the information is used to tell XCTest the exact line which caused +the failure so it can be highlighted in the IDE. + +@param fileName The name of the file where the failing code exists. +@param lineNumber The line number of the failing code. + +@return An EarlGreyImpl instance, with details of the code invoking EarlGrey. +*/static invokedFromFileLineNumber(fileName, lineNumber) { + if (typeof fileName !== "string") throw new Error("fileName should be a string, but got " + (fileName + (" (" + (typeof fileName + ")")))); + if (typeof lineNumber !== "number") throw new Error("lineNumber should be a number, but got " + (lineNumber + (" (" + (typeof lineNumber + ")")))); + return { + target: { + type: "Class", + value: "EarlGreyImpl" + }, + method: "invokedFromFile:lineNumber:", + args: [{ + type: "NSString", + value: fileName + }, { + type: "NSInteger", + value: lineNumber + }] + }; + } + + /*Rotate the device to a given @c deviceOrientation. All device orientations except for +@c UIDeviceOrientationUnknown are supported. If a non-nil @c errorOrNil is provided, it will +be populated with the failure reason if the orientation change fails, otherwise a test failure +will be registered. + +@param deviceOrientation The desired orientation of the device. +@param[out] errorOrNil Error that will be populated on failure. If @c nil, a test +failure will be reported if the rotation attempt fails. + +@return @c YES if the rotation was successful, @c NO otherwise. +*/static rotateDeviceToOrientationErrorOrNil(element, deviceOrientation) { + if (!["landscape", "portrait"].some(option => option === deviceOrientation)) throw new Error("deviceOrientation should be one of [landscape, portrait], but got " + deviceOrientation); + return { + target: element, + method: "rotateDeviceToOrientation:errorOrNil:", + args: [{ + type: "NSInteger", + value: sanitize_uiDeviceOrientation(deviceOrientation) + }] + }; + } + + /*Dismisses the keyboard by resigning the first responder, if any. Will populate the provided +error if the first responder is not present or if the keyboard is not visible. + +@param[out] errorOrNil Error that will be populated on failure. If @c nil, a test +failure will be reported if the dismissing fails. + +@return @c YES if the dismissing of the keyboard was successful, @c NO otherwise. +*/static dismissKeyboardWithError(element) { + return { + target: element, + method: "dismissKeyboardWithError:", + args: [] + }; + } + +} + +module.exports = EarlGreyImpl; \ No newline at end of file diff --git a/detox/src/ios/earlgreyapi/GREYCondition.js b/detox/src/ios/earlgreyapi/GREYCondition.js index 4db37b6c8a..b17a02a008 100644 --- a/detox/src/ios/earlgreyapi/GREYCondition.js +++ b/detox/src/ios/earlgreyapi/GREYCondition.js @@ -22,10 +22,7 @@ GREYCondition::waitWithTimeout:pollInterval: */static waitWithTimeout(element, seconds) { if (typeof seconds !== "number") throw new Error("seconds should be a number, but got " + (seconds + (" (" + (typeof seconds + ")")))); return { - target: { - type: "Invocation", - value: element - }, + target: element, method: "waitWithTimeout:", args: [{ type: "CGFloat", @@ -48,10 +45,7 @@ the condition as close as possible to every @c interval seconds. if (typeof seconds !== "number") throw new Error("seconds should be a number, but got " + (seconds + (" (" + (typeof seconds + ")")))); if (typeof interval !== "number") throw new Error("interval should be a number, but got " + (interval + (" (" + (typeof interval + ")")))); return { - target: { - type: "Invocation", - value: element - }, + target: element, method: "waitWithTimeout:pollInterval:", args: [{ type: "CGFloat", diff --git a/detox/src/ios/earlgreyapi/GREYConfiguration.js b/detox/src/ios/earlgreyapi/GREYConfiguration.js index 11cc205fd1..7314a21b3b 100644 --- a/detox/src/ios/earlgreyapi/GREYConfiguration.js +++ b/detox/src/ios/earlgreyapi/GREYConfiguration.js @@ -31,10 +31,7 @@ NSInvalidArgumentException is raised. */static valueForConfigKey(element, configKey) { if (typeof configKey !== "string") throw new Error("configKey should be a string, but got " + (configKey + (" (" + (typeof configKey + ")")))); return { - target: { - type: "Invocation", - value: element - }, + target: element, method: "valueForConfigKey:", args: [{ type: "NSString", @@ -55,10 +52,7 @@ raised. */static boolValueForConfigKey(element, configKey) { if (typeof configKey !== "string") throw new Error("configKey should be a string, but got " + (configKey + (" (" + (typeof configKey + ")")))); return { - target: { - type: "Invocation", - value: element - }, + target: element, method: "boolValueForConfigKey:", args: [{ type: "NSString", @@ -79,10 +73,7 @@ raised. */static integerValueForConfigKey(element, configKey) { if (typeof configKey !== "string") throw new Error("configKey should be a string, but got " + (configKey + (" (" + (typeof configKey + ")")))); return { - target: { - type: "Invocation", - value: element - }, + target: element, method: "integerValueForConfigKey:", args: [{ type: "NSString", @@ -103,10 +94,7 @@ raised. */static doubleValueForConfigKey(element, configKey) { if (typeof configKey !== "string") throw new Error("configKey should be a string, but got " + (configKey + (" (" + (typeof configKey + ")")))); return { - target: { - type: "Invocation", - value: element - }, + target: element, method: "doubleValueForConfigKey:", args: [{ type: "NSString", @@ -121,10 +109,7 @@ raised. are not reset. */static reset(element) { return { - target: { - type: "Invocation", - value: element - }, + target: element, method: "reset", args: [] }; @@ -140,10 +125,7 @@ Overwrites any previous value for the configuration. */static setValueForConfigKey(element, value, configKey) { if (typeof configKey !== "string") throw new Error("configKey should be a string, but got " + (configKey + (" (" + (typeof configKey + ")")))); return { - target: { - type: "Invocation", - value: element - }, + target: element, method: "setValue:forConfigKey:", args: [value, { type: "NSString", @@ -162,10 +144,7 @@ Overwrites any previous value for the configuration. */static setDefaultValueForConfigKey(element, value, configKey) { if (typeof configKey !== "string") throw new Error("configKey should be a string, but got " + (configKey + (" (" + (typeof configKey + ")")))); return { - target: { - type: "Invocation", - value: element - }, + target: element, method: "setDefaultValue:forConfigKey:", args: [value, { type: "NSString", diff --git a/detox/src/ios/earlgreyapi/GREYConfigurationDetox.js b/detox/src/ios/earlgreyapi/GREYConfigurationDetox.js index a8cc4b397d..c59075774a 100644 --- a/detox/src/ios/earlgreyapi/GREYConfigurationDetox.js +++ b/detox/src/ios/earlgreyapi/GREYConfigurationDetox.js @@ -9,10 +9,7 @@ class GREYConfiguration { static enableSynchronization(element) { return { - target: { - type: "Invocation", - value: element - }, + target: element, method: "enableSynchronization", args: [] }; @@ -20,10 +17,7 @@ class GREYConfiguration { static disableSynchronization(element) { return { - target: { - type: "Invocation", - value: element - }, + target: element, method: "disableSynchronization", args: [] }; diff --git a/detox/src/ios/earlgreyapi/GREYInteraction.js b/detox/src/ios/earlgreyapi/GREYInteraction.js index 3d2598e7bc..e448a177fd 100644 --- a/detox/src/ios/earlgreyapi/GREYInteraction.js +++ b/detox/src/ios/earlgreyapi/GREYInteraction.js @@ -20,10 +20,7 @@ will be performed on. } return { - target: { - type: "Invocation", - value: element - }, + target: element, method: "inRoot:", args: [rootMatcher] }; @@ -60,10 +57,7 @@ performAction:grey_tap()] // This should be separately called for the action. } return { - target: { - type: "Invocation", - value: element - }, + target: element, method: "usingSearchAction:onElementWithMatcher:", args: [action, matcher] }; @@ -81,15 +75,32 @@ performAction:grey_tap()] // This should be separately called for the action. } return { - target: { - type: "Invocation", - value: element - }, + target: element, method: "performAction:", args: [action] }; } + /*Performs an @c action on the selected UI element with an error set on failure. + +@param action The action to be performed on the @c element. +@param[out] errorOrNil Error populated on failure. +@throws NSException on action failure if @c errorOrNil is not set. + +@return The provided GREYInteraction instance, with an action and an error that will be +populated on failure. +*/static performActionError(element, action) { + if (typeof action !== "object" || action.type !== "Invocation" || typeof action.value !== "object" || typeof action.value.target !== "object" || action.value.target.value !== "GREYActions") { + throw new Error('action should be a GREYAction, but got ' + JSON.stringify(action)); + } + + return { + target: element, + method: "performAction:error:", + args: [action] + }; + } + /*Performs an assertion that evaluates @c matcher on the selected UI element. @param matcher The matcher to be evaluated on the @c element. @@ -101,15 +112,32 @@ performAction:grey_tap()] // This should be separately called for the action. } return { - target: { - type: "Invocation", - value: element - }, + target: element, method: "assertWithMatcher:", args: [matcher] }; } + /*Performs an assertion that evaluates @c matcher on the selected UI element. + +@param matcher The matcher to be evaluated on the @c element. +@param[out] errorOrNil Error populated on failure. +@throws NSException on assertion failure if @c errorOrNil is not set. + +@return The provided GREYInteraction instance, with a matcher to be evaluated on an element and +an error that will be populated on failure. +*/static assertWithMatcherError(element, 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: element, + method: "assertWithMatcher:error:", + args: [matcher] + }; + } + /*In case of multiple matches, selects the element at the specified index. In case of the index being over the number of matched elements, it throws an exception. Please make sure that this is used after you've created the matcher. For example, in case three elements are @@ -129,10 +157,7 @@ specified index in the list of matched elements. */static atIndex(element, index) { if (typeof index !== "number") throw new Error("index should be a number, but got " + (index + (" (" + (typeof index + ")")))); return { - target: { - type: "Invocation", - value: element - }, + target: element, method: "atIndex:", args: [{ type: "NSInteger", diff --git a/detox/src/ios/expect.js b/detox/src/ios/expect.js index bab14539d8..6800cf8b75 100644 --- a/detox/src/ios/expect.js +++ b/detox/src/ios/expect.js @@ -183,14 +183,14 @@ class ActionInteraction extends Interaction { constructor(element, action) { super(); - this._call = GreyInteraction.performAction(callThunk(element), callThunk(action)); + this._call = GreyInteraction.performAction(invoke.callDirectly(callThunk(element)), callThunk(action)); } } class MatcherAssertionInteraction extends Interaction { constructor(element, matcher) { super(); - this._call = GreyInteraction.assertWithMatcher(callThunk(element), callThunk(matcher)); + this._call = GreyInteraction.assertWithMatcher(invoke.callDirectly(callThunk(element)), callThunk(matcher)); } } @@ -219,7 +219,7 @@ class WaitForInteraction extends Interaction { _conditionCall = GreyConditionDetox.detoxConditionForNotElementMatched(callThunk(this._element)); } - this._call = GreyCondition.waitWithTimeout(_conditionCall, timeout / 1000) + this._call = GreyCondition.waitWithTimeout(invoke.callDirectly(_conditionCall), timeout / 1000) await this.execute(); } whileElement(searchMatcher) { @@ -239,9 +239,9 @@ class WaitForActionInteraction extends Interaction { } async _execute(searchAction) { - const _interactionCall = GreyInteraction.usingSearchActionOnElementWithMatcher(callThunk(this._element), callThunk(searchAction), callThunk(this._searchMatcher)); + const _interactionCall = GreyInteraction.usingSearchActionOnElementWithMatcher(invoke.callDirectly(callThunk(this._element)), callThunk(searchAction), callThunk(this._searchMatcher)); - this._call = GreyInteraction.assertWithMatcher(_interactionCall, callThunk(this._originalMatcher)); + this._call = GreyInteraction.assertWithMatcher(invoke.callDirectly(_interactionCall), callThunk(this._originalMatcher)); await this.execute(); } async scroll(amount, direction = 'down') { diff --git a/generation/__tests__/ios.js b/generation/__tests__/ios.js index 6e67d89405..aac2f58d45 100644 --- a/generation/__tests__/ios.js +++ b/generation/__tests__/ios.js @@ -300,8 +300,16 @@ describe('iOS generation', () => { expect(ExampleClass.performAction).toBeInstanceOf(Function); }); - it('should have the first arg set as invocation', () => { - expect(ExampleClass.performAction('MyClass', 'Foo')).toEqual({ + it("should have the first arg set as invocation", () => { + expect( + ExampleClass.performAction( + { + type: "Invocation", + value: "MyClass" + }, + "Foo" + ) + ).toEqual({ target: { type: 'Invocation', value: 'MyClass' @@ -317,6 +325,13 @@ describe('iOS generation', () => { }); }); + describe("filtered argument types", () => { + it("should remove them from the method", () => { + // gets the expected arguments + expect(ExampleClass.rotateDeviceToOrientationErrorOrNil.length).toBe(1); + }); + }); + afterAll(() => { // Clean up remove.removeSync('./__tests__/generated-ios'); diff --git a/generation/adapters/ios.js b/generation/adapters/ios.js index 30df668659..3167a60b91 100644 --- a/generation/adapters/ios.js +++ b/generation/adapters/ios.js @@ -31,7 +31,8 @@ const typeCheckInterfaces = { 'id': isGreyMatcher, 'GREYElementInteraction*': isGreyElementInteraction, UIAccessibilityTraits: isArray, - id: isDefined + id: isDefined, + UIDeviceOrientation: isOneOf(["landscape", "portrait"]) }; const contentSanitizersForType = { @@ -50,10 +51,15 @@ const contentSanitizersForType = { name: 'sanitize_uiAccessibilityTraits', value: callGlobal('sanitize_uiAccessibilityTraits') }, - 'GREYElementInteraction*': { - type: 'Invocation', - name: 'sanitize_greyElementInteraction', - value: callGlobal('sanitize_greyElementInteraction') + "GREYElementInteraction*": { + type: "Invocation", + name: "sanitize_greyElementInteraction", + value: callGlobal("sanitize_greyElementInteraction") + }, + UIDeviceOrientation: { + type: "NSInteger", + name: "sanitize_uiDeviceOrientation", + value: callGlobal("sanitize_uiDeviceOrientation") } }; @@ -62,20 +68,36 @@ module.exports = generator({ contentSanitizersForFunction: {}, contentSanitizersForType, supportedTypes: [ + 'CFTimeInterval', 'CGFloat', 'CGPoint', 'GREYContentEdge', 'GREYDirection', 'GREYElementInteraction*', + 'id', + 'id', + 'id', 'NSInteger', 'NSString *', 'NSString', 'NSUInteger', - 'id', - 'id', - 'CFTimeInterval', 'UIAccessibilityTraits', - 'id' + "__strong NSError **", + "CFTimeInterval", + "CGFloat", + "CGPoint", + "GREYContentEdge", + "GREYDirection", + "GREYElementInteraction*", + "id", + "id", + "id", + "NSInteger", + "NSString *", + "NSString", + "NSUInteger", + "UIAccessibilityTraits", + "UIDeviceOrientation", ], renameTypesMap: { NSUInteger: 'NSInteger', diff --git a/generation/core/generator.js b/generation/core/generator.js index 14687ac4e9..39f70c3f89 100644 --- a/generation/core/generator.js +++ b/generation/core/generator.js @@ -78,7 +78,13 @@ module.exports = function getGenerator({ ); } + const blacklistedArgumentTypes = ["__strong NSError **"]; + function filterBlacklistedArguments(arg) { + return !blacklistedArgumentTypes.includes(arg.type); + } + function createMethod(classJson, json) { + json.args = json.args.filter(filterBlacklistedArguments); const args = json.args.map(({ name }) => t.identifier(name)); if (!json.static) { @@ -169,20 +175,28 @@ module.exports = function getGenerator({ (arg) => shouldBeWrapped(arg) ? t.objectExpression([ - t.objectProperty(t.identifier('type'), t.stringLiteral(addArgumentTypeSanitizer(arg))), - t.objectProperty(t.identifier('value'), addArgumentContentSanitizerCall(arg, json.name)) - ]) + t.objectProperty(t.identifier('type'), t.stringLiteral(addArgumentTypeSanitizer(arg))), + t.objectProperty(t.identifier('value'), addArgumentContentSanitizerCall(arg, json.name)) + ]) : addArgumentContentSanitizerCall(arg, json.name) ); return t.returnStatement( t.objectExpression([ t.objectProperty( - t.identifier('target'), - t.objectExpression([ - t.objectProperty(t.identifier('type'), t.stringLiteral(json.static ? 'Class' : 'Invocation')), - t.objectProperty(t.identifier('value'), json.static ? t.stringLiteral(classValue(classJson)) : t.identifier('element')) - ]) + t.identifier("target"), + json.static + ? t.objectExpression([ + t.objectProperty( + t.identifier("type"), + t.stringLiteral("Class") + ), + t.objectProperty( + t.identifier("value"), + t.stringLiteral(classValue(classJson)) + ) + ]) + : t.identifier("element") ), t.objectProperty(t.identifier('method'), t.stringLiteral(json.name)), t.objectProperty(t.identifier('args'), t.arrayExpression(args)) diff --git a/generation/core/global-functions.js b/generation/core/global-functions.js index 8ac4a2dc22..28cc27a12b 100644 --- a/generation/core/global-functions.js +++ b/generation/core/global-functions.js @@ -138,6 +138,15 @@ function sanitize_greyElementInteraction(value) { }; } // END sanitize_greyElementInteraction +function sanitize_uiDeviceOrientation(value) { + const orientationMapping = { + landscape: 3, // top at left side landscape + portrait: 1 // non-reversed portrait + }; + + return orientationMapping[value]; +} // END sanitize_uiDeviceOrientation + module.exports = { sanitize_greyDirection, sanitize_greyContentEdge, @@ -145,5 +154,6 @@ module.exports = { sanitize_android_direction, sanitize_android_edge, sanitize_matcher, - sanitize_greyElementInteraction + sanitize_greyElementInteraction, + sanitize_uiDeviceOrientation }; diff --git a/generation/fixtures/example.h b/generation/fixtures/example.h index c8439750b5..cf78f8f8a0 100644 --- a/generation/fixtures/example.h +++ b/generation/fixtures/example.h @@ -67,4 +67,8 @@ // This method is an instance method // For us this means that we don't set the class type, but let it be set from the outside // This is an assumption based on our current experience with EarlGrey, we might need to rework this at some point -- (instancetype)performAction:(NSString *)action; \ No newline at end of file +- (instancetype)performAction:(NSString *)action; + +// This method has a blacklisted argument type, so it should only have one arg ++ (BOOL)rotateDeviceToOrientation:(UIDeviceOrientation)deviceOrientation + errorOrNil:(__strong NSError **)errorOrNil; \ No newline at end of file diff --git a/generation/index.js b/generation/index.js index b44e41b7cb..5d292c1e2e 100755 --- a/generation/index.js +++ b/generation/index.js @@ -16,7 +16,9 @@ const iosFiles = { "../detox/ios/Detox/GREYConfiguration+Detox.h": "../detox/src/ios/earlgreyapi/GREYConfigurationDetox.js", "../detox/ios/EarlGrey/EarlGrey/Common/GREYConfiguration.h": - "../detox/src/ios/earlgreyapi/GREYConfiguration.js" + "../detox/src/ios/earlgreyapi/GREYConfiguration.js", + "../detox/ios/EarlGrey/EarlGrey/EarlGrey.h": + "../detox/src/ios/earlgreyapi/EarlGrey.js" }; generateIOSAdapters(iosFiles);