diff --git a/detox/src/ios/earlgreyapi/GREYActions.js b/detox/src/ios/earlgreyapi/GREYActions.js index 2ea832c6bd..35fe48c80d 100644 --- a/detox/src/ios/earlgreyapi/GREYActions.js +++ b/detox/src/ios/earlgreyapi/GREYActions.js @@ -39,6 +39,33 @@ function sanitize_greyContentEdge(action) { } } +function sanitize_uiAccessibilityTraits(value) { + let traits = 0; + for (let i = 0; i < value.length; i++) { + switch (value[i]) { + case 'button': traits |= 1; break; + case 'link': traits |= 2; break; + case 'header': traits |= 4; break; + case 'search': traits |= 8; break; + case 'image': traits |= 16; break; + case 'selected': traits |= 32; break; + case 'plays': traits |= 64; break; + case 'key': traits |= 128; break; + case 'text': traits |= 256; break; + case 'summary': traits |= 512; break; + case 'disabled': traits |= 1024; break; + case 'frequentUpdates': traits |= 2048; break; + case 'startsMedia': traits |= 4096; break; + case 'adjustable': traits |= 8192; break; + case 'allowsDirectInteraction': traits |= 16384; break; + case 'pageTurn': traits |= 32768; break; + default: throw new Error(`Unknown trait '${value[i]}', see list in https://facebook.github.io/react-native/docs/accessibility.html#accessibilitytraits-ios`); + } + } + + return traits; +} + class GREYActions { diff --git a/detox/src/ios/earlgreyapi/GREYMatchers+Detox.js b/detox/src/ios/earlgreyapi/GREYMatchers+Detox.js index 7ccec743fc..2cb46e1fa6 100644 --- a/detox/src/ios/earlgreyapi/GREYMatchers+Detox.js +++ b/detox/src/ios/earlgreyapi/GREYMatchers+Detox.js @@ -39,6 +39,33 @@ function sanitize_greyContentEdge(action) { } } +function sanitize_uiAccessibilityTraits(value) { + let traits = 0; + for (let i = 0; i < value.length; i++) { + switch (value[i]) { + case 'button': traits |= 1; break; + case 'link': traits |= 2; break; + case 'header': traits |= 4; break; + case 'search': traits |= 8; break; + case 'image': traits |= 16; break; + case 'selected': traits |= 32; break; + case 'plays': traits |= 64; break; + case 'key': traits |= 128; break; + case 'text': traits |= 256; break; + case 'summary': traits |= 512; break; + case 'disabled': traits |= 1024; break; + case 'frequentUpdates': traits |= 2048; break; + case 'startsMedia': traits |= 4096; break; + case 'adjustable': traits |= 8192; break; + case 'allowsDirectInteraction': traits |= 16384; break; + case 'pageTurn': traits |= 32768; break; + default: throw new Error(`Unknown trait '${value[i]}', see list in https://facebook.github.io/react-native/docs/accessibility.html#accessibilitytraits-ios`); + } + } + + return traits; +} + class GREYMatchers { diff --git a/detox/src/ios/earlgreyapi/GREYMatchers.js b/detox/src/ios/earlgreyapi/GREYMatchers.js index 17dbebb7ef..2b8ab723e5 100644 --- a/detox/src/ios/earlgreyapi/GREYMatchers.js +++ b/detox/src/ios/earlgreyapi/GREYMatchers.js @@ -39,6 +39,33 @@ function sanitize_greyContentEdge(action) { } } +function sanitize_uiAccessibilityTraits(value) { + let traits = 0; + for (let i = 0; i < value.length; i++) { + switch (value[i]) { + case 'button': traits |= 1; break; + case 'link': traits |= 2; break; + case 'header': traits |= 4; break; + case 'search': traits |= 8; break; + case 'image': traits |= 16; break; + case 'selected': traits |= 32; break; + case 'plays': traits |= 64; break; + case 'key': traits |= 128; break; + case 'text': traits |= 256; break; + case 'summary': traits |= 512; break; + case 'disabled': traits |= 1024; break; + case 'frequentUpdates': traits |= 2048; break; + case 'startsMedia': traits |= 4096; break; + case 'adjustable': traits |= 8192; break; + case 'allowsDirectInteraction': traits |= 16384; break; + case 'pageTurn': traits |= 32768; break; + default: throw new Error(`Unknown trait '${value[i]}', see list in https://facebook.github.io/react-native/docs/accessibility.html#accessibilitytraits-ios`); + } + } + + return traits; +} + class GREYMatchers { @@ -116,6 +143,30 @@ class GREYMatchers { }; } + /*Matcher for UI element with the provided accessibility @c traits. + +@param traits The accessibility traits to be matched. + +@return A matcher for the accessibility traits of an accessible element. + +*/static matcherForAccessibilityTraits(traits) { + if (typeof traits !== 'object' || !traits instanceof Array) { + throw new Error('TraitsMatcher ctor argument must be an array, got ' + typeof traits); + } + + return { + target: { + type: "Class", + value: "GREYMatchers" + }, + method: "matcherForAccessibilityTraits:", + args: [{ + type: "NSInteger", + value: sanitize_uiAccessibilityTraits(traits) + }] + }; + } + /*Matcher for UI element with the provided accessiblity @c hint. @param hint The accessibility hint to be matched. diff --git a/detox/src/ios/matchers.js b/detox/src/ios/matchers.js index a0be4da18f..aefc203176 100644 --- a/detox/src/ios/matchers.js +++ b/detox/src/ios/matchers.js @@ -59,30 +59,7 @@ class TypeMatcher extends Matcher { class TraitsMatcher extends Matcher { constructor(value) { super(); - if ((typeof value !== 'object') || (!value instanceof Array)) throw new Error(`TraitsMatcher ctor argument must be an array, got ${typeof value}`); - let traits = 0; - for (let i = 0; i < value.length; i++) { - switch (value[i]) { - case 'button': traits |= 1; break; - case 'link': traits |= 2; break; - case 'header': traits |= 4; break; - case 'search': traits |= 8; break; - case 'image': traits |= 16; break; - case 'selected': traits |= 32; break; - case 'plays': traits |= 64; break; - case 'key': traits |= 128; break; - case 'text': traits |= 256; break; - case 'summary': traits |= 512; break; - case 'disabled': traits |= 1024; break; - case 'frequentUpdates': traits |= 2048; break; - case 'startsMedia': traits |= 4096; break; - case 'adjustable': traits |= 8192; break; - case 'allowsDirectInteraction': traits |= 16384; break; - case 'pageTurn': traits |= 32768; break; - default: throw new Error(`Unknown trait '${value[i]}', see list in https://facebook.github.io/react-native/docs/accessibility.html#accessibilitytraits-ios`); - } - } - this._call = invoke.call(invoke.IOS.Class('GREYMatchers'), 'matcherForAccessibilityTraits:', invoke.IOS.NSInteger(traits)); + this._call = invoke.callDirectly(GreyMatchers.matcherForAccessibilityTraits(value)); } } diff --git a/detox/test/e2e/b-matchers.js b/detox/test/e2e/b-matchers.js index 81aef3fa4d..b169bae3c4 100644 --- a/detox/test/e2e/b-matchers.js +++ b/detox/test/e2e/b-matchers.js @@ -57,6 +57,7 @@ describe('Matchers', () => { await expect(element(by.id('UniqueId345').and(by.text('ID')))).toExist(); await expect(element(by.id('UniqueId345').and(by.text('RandomJunk')))).toNotExist(); await expect(element(by.id('UniqueId345').and(by.label('RandomJunk')))).toNotExist(); + await expect(element(by.id('UniqueId345').and(by.traits(['button'])))).toNotExist(); }); // waiting to upgrade EarlGrey version in order to test this (not supported in our current one) diff --git a/generation/__tests__/__snapshots__/global-functions.js.snap b/generation/__tests__/__snapshots__/global-functions.js.snap index b34c8b5203..2c8fd64d54 100644 --- a/generation/__tests__/__snapshots__/global-functions.js.snap +++ b/generation/__tests__/__snapshots__/global-functions.js.snap @@ -3,3 +3,5 @@ exports[`globals sanitize_greyContentEdge should fail with unknown value 1`] = `"GREYAction.GREYContentEdge must be a 'left'/'right'/'top'/'bottom', got kittens"`; exports[`globals sanitize_greyDirection should fail with unknown value 1`] = `"GREYAction.GREYDirection must be a 'left'/'right'/'up'/'down', got kittens"`; + +exports[`globals sanitize_uiAccessibilityTraits should throw if unknown trait is accessed 1`] = `"Unknown trait 'unknown', see list in https://facebook.github.io/react-native/docs/accessibility.html#accessibilitytraits-ios"`; diff --git a/generation/__tests__/global-functions.js b/generation/__tests__/global-functions.js index 34dd3900d2..71e49527f0 100644 --- a/generation/__tests__/global-functions.js +++ b/generation/__tests__/global-functions.js @@ -30,4 +30,15 @@ describe("globals", () => { }).toThrowErrorMatchingSnapshot(); }); }); + + describe("sanitize_uiAccessibilityTraits", () => { + it("should return numbers for traits", () => { + expect(globals.sanitize_uiAccessibilityTraits(["button", "link"])).toBe(3); + expect(globals.sanitize_uiAccessibilityTraits(["summary", "allowsDirectInteraction"])).toBe(16896); + }); + + it("should throw if unknown trait is accessed", () => { + expect(() => globals.sanitize_uiAccessibilityTraits(["unknown"])).toThrowErrorMatchingSnapshot(); + }); + }); }); \ No newline at end of file diff --git a/generation/earl-grey/global-functions.js b/generation/earl-grey/global-functions.js index 5720d3ec1f..44f6360013 100644 --- a/generation/earl-grey/global-functions.js +++ b/generation/earl-grey/global-functions.js @@ -32,8 +32,36 @@ function sanitize_greyContentEdge(action) { } } +function sanitize_uiAccessibilityTraits(value) { + let traits = 0; + for (let i = 0; i < value.length; i++) { + switch (value[i]) { + case 'button': traits |= 1; break; + case 'link': traits |= 2; break; + case 'header': traits |= 4; break; + case 'search': traits |= 8; break; + case 'image': traits |= 16; break; + case 'selected': traits |= 32; break; + case 'plays': traits |= 64; break; + case 'key': traits |= 128; break; + case 'text': traits |= 256; break; + case 'summary': traits |= 512; break; + case 'disabled': traits |= 1024; break; + case 'frequentUpdates': traits |= 2048; break; + case 'startsMedia': traits |= 4096; break; + case 'adjustable': traits |= 8192; break; + case 'allowsDirectInteraction': traits |= 16384; break; + case 'pageTurn': traits |= 32768; break; + default: throw new Error(`Unknown trait '${value[i]}', see list in https://facebook.github.io/react-native/docs/accessibility.html#accessibilitytraits-ios`); + } + } + + return traits; +} + module.exports = { sanitize_greyDirection, sanitize_greyContentEdge, + sanitize_uiAccessibilityTraits, }; \ No newline at end of file diff --git a/generation/earl-grey/index.js b/generation/earl-grey/index.js index 0f27be9e96..8c55c71296 100644 --- a/generation/earl-grey/index.js +++ b/generation/earl-grey/index.js @@ -32,7 +32,18 @@ const isGreyMatcher = ({ name }) => template(` } `)({ ARG: t.identifier(name) - }) + }); +const isArray = ({ name }) => template(` +if ( + (typeof ARG !== 'object') || + (!ARG instanceof Array) +) { + throw new Error('TraitsMatcher ctor argument must be an array, got ' + typeof ARG); + } +`)({ + ARG: t.identifier(name) + }); + // Constants const SUPPORTED_TYPES = [ @@ -44,7 +55,8 @@ const SUPPORTED_TYPES = [ "NSString *", "NSString", "NSUInteger", - "id" + "id", + "UIAccessibilityTraits" ]; /** @@ -170,6 +182,10 @@ const supportedContentSanitizersMap = { GREYContentEdge: { type: "NSInteger", value: callGlobal("sanitize_greyContentEdge") + }, + UIAccessibilityTraits: { + type: "NSInteger", + value: callGlobal("sanitize_uiAccessibilityTraits") } }; function addArgumentContentSanitizerCall(json) { @@ -236,6 +252,7 @@ function createTypeCheck(json) { GREYContentEdge: isOneOf(["left", "right", "top", "bottom"]), GREYPinchDirection: isOneOf(["outward", "inward"]), "id": isGreyMatcher, + UIAccessibilityTraits: isArray, }; const typeCheckCreator = typeInterfaces[json.type];