diff --git a/detox/src/android/espressoapi/DetoxMatcher.js b/detox/src/android/espressoapi/DetoxMatcher.js index a723d2a536..cc2107a693 100644 --- a/detox/src/android/espressoapi/DetoxMatcher.js +++ b/detox/src/android/espressoapi/DetoxMatcher.js @@ -5,7 +5,11 @@ */ - +function sanitize_matcher(matcher) { + const originalMatcher = + typeof matcher._call === "function" ? matcher._call() : matcher._call; + return originalMatcher.type ? originalMatcher.value : originalMatcher; +} class DetoxMatcher { static matcherForText(text) { if (typeof text !== "string") throw new Error("text should be a string, but got " + (text + (" (" + (typeof text + ")")))); @@ -15,10 +19,7 @@ class DetoxMatcher { value: "com.wix.detox.espresso.DetoxMatcher" }, method: "matcherForText", - args: [{ - type: "String", - value: text - }] + args: [text] }; } @@ -30,10 +31,7 @@ class DetoxMatcher { value: "com.wix.detox.espresso.DetoxMatcher" }, method: "matcherForContentDescription", - args: [{ - type: "String", - value: contentDescription - }] + args: [contentDescription] }; } @@ -45,9 +43,142 @@ class DetoxMatcher { value: "com.wix.detox.espresso.DetoxMatcher" }, method: "matcherForTestId", + args: [testId] + }; + } + + static matcherForAnd(m1, m2) { + if (typeof m1 !== 'object' || typeof m1.constructor !== 'function' || m1.constructor.name.indexOf('Matcher') === -1) { + const isObject = typeof m1 === 'object'; + const additionalErrorInfo = isObject ? typeof m1.constructor === 'object' ? 'the constructor is no object' : 'it has a wrong class name: "' + m1.constructor.name + '"' : 'it is no object'; + throw new Error('m1 should be an instance of Matcher, got "' + m1 + '", it appears that ' + additionalErrorInfo); + } + + if (typeof m2 !== 'object' || typeof m2.constructor !== 'function' || m2.constructor.name.indexOf('Matcher') === -1) { + const isObject = typeof m2 === 'object'; + const additionalErrorInfo = isObject ? typeof m2.constructor === 'object' ? 'the constructor is no object' : 'it has a wrong class name: "' + m2.constructor.name + '"' : 'it is no object'; + throw new Error('m2 should be an instance of Matcher, got "' + m2 + '", it appears that ' + additionalErrorInfo); + } + + return { + target: { + type: "Class", + value: "com.wix.detox.espresso.DetoxMatcher" + }, + method: "matcherForAnd", + args: [{ + type: "Invocation", + value: sanitize_matcher(m1) + }, { + type: "Invocation", + value: sanitize_matcher(m2) + }] + }; + } + + static matcherForOr(m1, m2) { + if (typeof m1 !== 'object' || typeof m1.constructor !== 'function' || m1.constructor.name.indexOf('Matcher') === -1) { + const isObject = typeof m1 === 'object'; + const additionalErrorInfo = isObject ? typeof m1.constructor === 'object' ? 'the constructor is no object' : 'it has a wrong class name: "' + m1.constructor.name + '"' : 'it is no object'; + throw new Error('m1 should be an instance of Matcher, got "' + m1 + '", it appears that ' + additionalErrorInfo); + } + + if (typeof m2 !== 'object' || typeof m2.constructor !== 'function' || m2.constructor.name.indexOf('Matcher') === -1) { + const isObject = typeof m2 === 'object'; + const additionalErrorInfo = isObject ? typeof m2.constructor === 'object' ? 'the constructor is no object' : 'it has a wrong class name: "' + m2.constructor.name + '"' : 'it is no object'; + throw new Error('m2 should be an instance of Matcher, got "' + m2 + '", it appears that ' + additionalErrorInfo); + } + + return { + target: { + type: "Class", + value: "com.wix.detox.espresso.DetoxMatcher" + }, + method: "matcherForOr", + args: [{ + type: "Invocation", + value: sanitize_matcher(m1) + }, { + type: "Invocation", + value: sanitize_matcher(m2) + }] + }; + } + + static matcherForNot(m) { + if (typeof m !== 'object' || typeof m.constructor !== 'function' || m.constructor.name.indexOf('Matcher') === -1) { + const isObject = typeof m === 'object'; + const additionalErrorInfo = isObject ? typeof m.constructor === 'object' ? 'the constructor is no object' : 'it has a wrong class name: "' + m.constructor.name + '"' : 'it is no object'; + throw new Error('m should be an instance of Matcher, got "' + m + '", it appears that ' + additionalErrorInfo); + } + + return { + target: { + type: "Class", + value: "com.wix.detox.espresso.DetoxMatcher" + }, + method: "matcherForNot", args: [{ - type: "String", - value: testId + type: "Invocation", + value: sanitize_matcher(m) + }] + }; + } + + static matcherWithAncestor(m, ancestorMatcher) { + if (typeof m !== 'object' || typeof m.constructor !== 'function' || m.constructor.name.indexOf('Matcher') === -1) { + const isObject = typeof m === 'object'; + const additionalErrorInfo = isObject ? typeof m.constructor === 'object' ? 'the constructor is no object' : 'it has a wrong class name: "' + m.constructor.name + '"' : 'it is no object'; + throw new Error('m should be an instance of Matcher, got "' + m + '", it appears that ' + additionalErrorInfo); + } + + if (typeof ancestorMatcher !== 'object' || typeof ancestorMatcher.constructor !== 'function' || ancestorMatcher.constructor.name.indexOf('Matcher') === -1) { + const isObject = typeof ancestorMatcher === 'object'; + const additionalErrorInfo = isObject ? typeof ancestorMatcher.constructor === 'object' ? 'the constructor is no object' : 'it has a wrong class name: "' + ancestorMatcher.constructor.name + '"' : 'it is no object'; + throw new Error('ancestorMatcher should be an instance of Matcher, got "' + ancestorMatcher + '", it appears that ' + additionalErrorInfo); + } + + return { + target: { + type: "Class", + value: "com.wix.detox.espresso.DetoxMatcher" + }, + method: "matcherWithAncestor", + args: [{ + type: "Invocation", + value: sanitize_matcher(m) + }, { + type: "Invocation", + value: sanitize_matcher(ancestorMatcher) + }] + }; + } + + static matcherWithDescendant(m, descendantMatcher) { + if (typeof m !== 'object' || typeof m.constructor !== 'function' || m.constructor.name.indexOf('Matcher') === -1) { + const isObject = typeof m === 'object'; + const additionalErrorInfo = isObject ? typeof m.constructor === 'object' ? 'the constructor is no object' : 'it has a wrong class name: "' + m.constructor.name + '"' : 'it is no object'; + throw new Error('m should be an instance of Matcher, got "' + m + '", it appears that ' + additionalErrorInfo); + } + + if (typeof descendantMatcher !== 'object' || typeof descendantMatcher.constructor !== 'function' || descendantMatcher.constructor.name.indexOf('Matcher') === -1) { + const isObject = typeof descendantMatcher === 'object'; + const additionalErrorInfo = isObject ? typeof descendantMatcher.constructor === 'object' ? 'the constructor is no object' : 'it has a wrong class name: "' + descendantMatcher.constructor.name + '"' : 'it is no object'; + throw new Error('descendantMatcher should be an instance of Matcher, got "' + descendantMatcher + '", it appears that ' + additionalErrorInfo); + } + + return { + target: { + type: "Class", + value: "com.wix.detox.espresso.DetoxMatcher" + }, + method: "matcherWithDescendant", + args: [{ + type: "Invocation", + value: sanitize_matcher(m) + }, { + type: "Invocation", + value: sanitize_matcher(descendantMatcher) }] }; } @@ -60,10 +191,7 @@ class DetoxMatcher { value: "com.wix.detox.espresso.DetoxMatcher" }, method: "matcherForClass", - args: [{ - type: "String", - value: className - }] + args: [className] }; } @@ -111,6 +239,31 @@ class DetoxMatcher { }; } + static matcherForAtIndex(index, innerMatcher) { + if (typeof index !== "number") throw new Error("index should be a number, but got " + (index + (" (" + (typeof index + ")")))); + + if (typeof innerMatcher !== 'object' || typeof innerMatcher.constructor !== 'function' || innerMatcher.constructor.name.indexOf('Matcher') === -1) { + const isObject = typeof innerMatcher === 'object'; + const additionalErrorInfo = isObject ? typeof innerMatcher.constructor === 'object' ? 'the constructor is no object' : 'it has a wrong class name: "' + innerMatcher.constructor.name + '"' : 'it is no object'; + throw new Error('innerMatcher should be an instance of Matcher, got "' + innerMatcher + '", it appears that ' + additionalErrorInfo); + } + + return { + target: { + type: "Class", + value: "com.wix.detox.espresso.DetoxMatcher" + }, + method: "matcherForAtIndex", + args: [{ + type: "Integer", + value: index + }, { + type: "Invocation", + value: sanitize_matcher(innerMatcher) + }] + }; + } + static matcherForAnything() { return { target: { diff --git a/detox/src/android/espressoapi/ViewActions.js b/detox/src/android/espressoapi/ViewActions.js index 261d923c81..f01f12c6d9 100644 --- a/detox/src/android/espressoapi/ViewActions.js +++ b/detox/src/android/espressoapi/ViewActions.js @@ -184,10 +184,7 @@ class ViewActions { value: "android.support.test.espresso.action.ViewActions" }, method: "typeTextIntoFocusedView", - args: [{ - type: "String", - value: stringToBeTyped - }] + args: [stringToBeTyped] }; } @@ -199,10 +196,7 @@ class ViewActions { value: "android.support.test.espresso.action.ViewActions" }, method: "typeText", - args: [{ - type: "String", - value: stringToBeTyped - }] + args: [stringToBeTyped] }; } @@ -214,10 +208,7 @@ class ViewActions { value: "android.support.test.espresso.action.ViewActions" }, method: "replaceText", - args: [{ - type: "String", - value: stringToBeSet - }] + args: [stringToBeSet] }; } @@ -229,10 +220,7 @@ class ViewActions { value: "android.support.test.espresso.action.ViewActions" }, method: "openLinkWithText", - args: [{ - type: "String", - value: linkText - }] + args: [linkText] }; } @@ -244,10 +232,7 @@ class ViewActions { value: "android.support.test.espresso.action.ViewActions" }, method: "openLinkWithUri", - args: [{ - type: "String", - value: uri - }] + args: [uri] }; } diff --git a/detox/src/android/matcher.js b/detox/src/android/matcher.js index 783eabced1..a030ae6226 100644 --- a/detox/src/android/matcher.js +++ b/detox/src/android/matcher.js @@ -5,32 +5,23 @@ const DetoxMatcher = 'com.wix.detox.espresso.DetoxMatcher'; 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.Android.Class(DetoxMatcher), 'matcherWithAncestor', _originalMatcherCall, matcher._call); + this._call = invoke.callDirectly(DetoxMatcherApi.matcherWithAncestor(this, matcher)); 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.Android.Class(DetoxMatcher), 'matcherWithDescendant', _originalMatcherCall, matcher._call); + this._call = invoke.callDirectly(DetoxMatcherApi.matcherWithDescendant(this, matcher)); 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.Android.Class(DetoxMatcher), 'matcherForAnd', _originalMatcherCall, matcher._call); + this._call = invoke.callDirectly(DetoxMatcherApi.matcherForAnd(this, matcher)); return this; } or(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.Android.Class(DetoxMatcher), 'matcherForOr', _originalMatcherCall, matcher._call); + this._call = invoke.callDirectly(DetoxMatcherApi.matcherForOr(this, matcher)); return this; } not() { - const _originalMatcherCall = this._call; - this._call = invoke.call(invoke.Android.Class(DetoxMatcher), 'matcherForNot', _originalMatcherCall); + this._call = invoke.callDirectly(DetoxMatcherApi.matcherForNot(this)); return this; } @@ -62,16 +53,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.Android.Class(DetoxMatcher), 'matcherForTestId', value); + this._call = invoke.callDirectly(DetoxMatcherApi.matcherForTestId(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.Android.Class(DetoxMatcher), 'matcherForClass', value); + this._call = invoke.callDirectly(DetoxMatcherApi.matcherForClass(value)); } } @@ -106,16 +95,14 @@ class NotExistsMatcher extends Matcher { 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.Android.Class(DetoxMatcher), 'matcherForText', value); + this._call = invoke.callDirectly(DetoxMatcherApi.matcherForText(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.Android.Class(DetoxMatcher), 'matcherForContentDescription', value); + this._call = invoke.callDirectly(DetoxMatcherApi.matcherForContentDescription(value)); } } diff --git a/detox/test/package.json b/detox/test/package.json index a468bbd185..69d793fecf 100644 --- a/detox/test/package.json +++ b/detox/test/package.json @@ -59,7 +59,7 @@ "binaryPath": "android/app/build/outputs/apk/release/app-release.apk", "build": "cd android && ./gradlew assembleRelease assembleAndroidTest -DtestBuildType=release && cd ..", "type": "android.emulator", - "name": "Nexus_5X_API_26" + "name": "Nexus_5X_API_27_x86" } } } diff --git a/generation/__tests__/__snapshots__/android.js.snap b/generation/__tests__/__snapshots__/android.js.snap index 6ff5be163f..a3e3aa9439 100644 --- a/generation/__tests__/__snapshots__/android.js.snap +++ b/generation/__tests__/__snapshots__/android.js.snap @@ -37,3 +37,7 @@ Object { }, } `; + +exports[`Android generation validation Matcher should fail getting no object 1`] = `"m1 should be an instance of Matcher, got \\"I am a string\\", it appears that it is no object"`; + +exports[`Android generation validation Matcher should fail with a wrong class 1`] = `"m1 should be an instance of Matcher, got \\"[object Object]\\", it appears that it has a wrong class name: \\"AnotherClass\\""`; diff --git a/generation/__tests__/android.js b/generation/__tests__/android.js index 9ff7a99a7b..fad556b06c 100644 --- a/generation/__tests__/android.js +++ b/generation/__tests__/android.js @@ -65,4 +65,40 @@ describe("Android generation", () => { expect(fn("down", 42)).toMatchSnapshot(); }); }); + + describe("validation", () => { + describe("Matcher", () => { + it("should fail getting no object", () => { + expect(() => { + ExampleClass.matcherForAnd("I am a string", "I am one too"); + }).toThrowErrorMatchingSnapshot(); + }); + + it("should fail with a wrong class", () => { + class AnotherClass {} + const x = new AnotherClass(); + + expect(() => { + ExampleClass.matcherForAnd(x, x); + }).toThrowErrorMatchingSnapshot(); + }); + + it("should succeed with the 'right' class", () => { + // stub for matcher class + class Matcher { + _call() { + return { + target: { type: "Class", value: "Detox.Matcher" }, + method: "matchNicely" + }; + } + } + + const m = new Matcher(); + expect(() => { + ExampleClass.matcherForAnd(m, m); + }).not.toThrow(); + }); + }); + }); }); diff --git a/generation/__tests__/global-functions.js b/generation/__tests__/global-functions.js index 088cf49eba..d07047094c 100644 --- a/generation/__tests__/global-functions.js +++ b/generation/__tests__/global-functions.js @@ -103,4 +103,39 @@ describe("globals", () => { ).toThrowErrorMatchingSnapshot(); }); }); + + const matcherInvocation = { + target: { type: "Class", value: "Detox.Matcher" }, + method: "matchNicely" + }; + describe("sanitize_matcher", () => { + it("should return the object if it's no function", () => { + expect(globals.sanitize_matcher({ _call: matcherInvocation })).toEqual( + matcherInvocation + ); + }); + + it("should return the ._call property if it's a function", () => { + const matcherLikeObj = { _call: () => matcherInvocation }; + expect(globals.sanitize_matcher(matcherLikeObj)).toEqual( + matcherInvocation + ); + }); + + it("should unwrap the object if it's in an invocation", () => { + const invoke = { type: "Invocation", value: matcherInvocation }; + const invokeCalled = { _call: invoke }; + const invokeThunk = { _call: () => invoke }; + + expect(globals.sanitize_matcher(invokeCalled)).toEqual(matcherInvocation); + expect(globals.sanitize_matcher(invokeThunk)).toEqual(matcherInvocation); + }); + + it("should not call on string", () => { + const matcherLikeObj = { + _call: "I am a call" + }; + expect(globals.sanitize_matcher(matcherLikeObj)).toBe("I am a call"); + }); + }); }); diff --git a/generation/adapters/android.js b/generation/adapters/android.js index f395be5337..1e91e7fb45 100644 --- a/generation/adapters/android.js +++ b/generation/adapters/android.js @@ -1,14 +1,20 @@ const t = require("babel-types"); const generator = require("../core/generator"); - -const { isNumber, isString, isBoolean } = require("../core/type-checks"); const { callGlobal } = require("../helpers"); +const { + isNumber, + isString, + isBoolean, + isOfClass +} = require("../core/type-checks"); + const typeCheckInterfaces = { Integer: isNumber, Double: isNumber, String: isString, - boolean: isBoolean + boolean: isBoolean, + "Matcher": isOfClass("Matcher") }; const contentSanitizersForFunction = { @@ -29,14 +35,35 @@ const contentSanitizersForFunction = { newType: "String", name: "sanitize_android_edge", value: callGlobal("sanitize_android_edge") + }, + "Matcher": { + type: "String", + name: "sanitize_matcher", + value: callGlobal("sanitize_matcher") + } +}; + +const contentSanitizersForType = { + "Matcher": { + type: "Invocation", + name: "sanitize_matcher", + value: callGlobal("sanitize_matcher") } }; module.exports = generator({ typeCheckInterfaces, - contentSanitizersForType: {}, contentSanitizersForFunction, - supportedTypes: ["Integer", "int", "double", "String", "boolean"], + contentSanitizersForType, + supportedTypes: [ + "Integer", + "int", + "double", + "Double", + "boolean", + "String", + "Matcher" + ], renameTypesMap: { int: "Integer", // TODO: add test double: "Double" diff --git a/generation/core/generator.js b/generation/core/generator.js index 769736f48a..e3f676c3ec 100644 --- a/generation/core/generator.js +++ b/generation/core/generator.js @@ -163,7 +163,7 @@ module.exports = function({ } // These types need no wrapping with {type: ..., value: } - const plainArgumentTypes = ["id"]; + const plainArgumentTypes = ["id", "String"]; function shouldBeWrapped({ type }) { return !plainArgumentTypes.includes(type); } diff --git a/generation/core/global-functions.js b/generation/core/global-functions.js index 44548d46ca..8ff3a0b815 100644 --- a/generation/core/global-functions.js +++ b/generation/core/global-functions.js @@ -136,10 +136,17 @@ function sanitize_uiAccessibilityTraits(value) { return traits; } // END sanitize_uiAccessibilityTraits +function sanitize_matcher(matcher) { + const originalMatcher = + typeof matcher._call === "function" ? matcher._call() : matcher._call; + return originalMatcher.type ? originalMatcher.value : originalMatcher; +} // END sanitize_matcher + module.exports = { sanitize_greyDirection, sanitize_greyContentEdge, sanitize_uiAccessibilityTraits, sanitize_android_direction, - sanitize_android_edge + sanitize_android_edge, + sanitize_matcher }; diff --git a/generation/core/type-checks.js b/generation/core/type-checks.js index 9deeea8704..cd0313f76b 100644 --- a/generation/core/type-checks.js +++ b/generation/core/type-checks.js @@ -40,6 +40,22 @@ if ( ARG: t.identifier(name) }); +const isOfClass = className => ({ name }) => + template(` + if ( + typeof ARG !== 'object' || + typeof ARG.constructor !== 'function' || + ARG.constructor.name.indexOf('${className}') === -1 + ) { + const isObject = typeof ARG === 'object'; + const additionalErrorInfo = isObject ? (typeof ARG.constructor === 'object' ? 'the constructor is no object' : 'it has a wrong class name: "' + ARG.constructor.name +'"') : 'it is no object'; + + throw new Error('${name} should be an instance of ${className}, got "' + ARG + '", it appears that ' + additionalErrorInfo); + } + `)({ + ARG: t.identifier(name) + }); + module.exports = { isNumber, isString, @@ -47,5 +63,6 @@ module.exports = { isPoint, isOneOf, isGreyMatcher, - isArray + isArray, + isOfClass }; diff --git a/generation/fixtures/example.java b/generation/fixtures/example.java index 04ad13b187..d0dcde57a2 100644 --- a/generation/fixtures/example.java +++ b/generation/fixtures/example.java @@ -226,4 +226,8 @@ public float[] calculateCoordinates(View view) { }; } + public static Matcher matcherForAnd(Matcher m1, Matcher m2) { + return allOf(m1, m2); + } + } \ No newline at end of file