@react-native-ama/core 1.2.0 → 2.0.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) hide show
  1. package/dist/ReactNativeAma.types.d.ts +66 -0
  2. package/dist/ReactNativeAma.types.js +2 -0
  3. package/dist/ReactNativeAmaModule.d.ts +11 -0
  4. package/dist/ReactNativeAmaModule.js +5 -0
  5. package/dist/ReactNativeAmaModule.web.d.ts +9 -0
  6. package/dist/ReactNativeAmaModule.web.js +27 -0
  7. package/dist/ReactNativeAmaView.d.ts +3 -0
  8. package/dist/ReactNativeAmaView.js +32 -0
  9. package/dist/ReactNativeAmaView.web.d.ts +3 -0
  10. package/dist/ReactNativeAmaView.web.js +34 -0
  11. package/dist/{src/components → components}/AMAProvider.d.ts +4 -2
  12. package/dist/{src/components → components}/AMAProvider.js +19 -45
  13. package/dist/{src/components → components}/AutofocusContainer.d.ts +2 -1
  14. package/dist/{src/components → components}/HideChildrenFromAccessibilityTree.d.ts +1 -1
  15. package/dist/{src/components → components}/HideChildrenFromAccessibilityTree.js +7 -5
  16. package/dist/{src/hooks → hooks}/useFocus.js +10 -3
  17. package/dist/{src/index.d.ts → index.d.ts} +3 -2
  18. package/dist/index.js +38 -0
  19. package/dist/internals/checks/checkAriaLabel.d.ts +3 -0
  20. package/dist/internals/checks/checkAriaLabel.js +15 -0
  21. package/dist/internals/checks/checkAriaRole.d.ts +3 -0
  22. package/dist/internals/checks/checkAriaRole.js +48 -0
  23. package/dist/internals/checks/checkContrast.d.ts +3 -0
  24. package/dist/internals/checks/checkContrast.js +84 -0
  25. package/dist/internals/checks/checkImageAlt.d.ts +3 -0
  26. package/dist/internals/checks/checkImageAlt.js +20 -0
  27. package/dist/internals/checks/checkIsUppercase.d.ts +8 -0
  28. package/dist/internals/checks/checkIsUppercase.js +30 -0
  29. package/dist/internals/checks/checkLongNumber.d.ts +3 -0
  30. package/dist/internals/checks/checkLongNumber.js +31 -0
  31. package/dist/internals/checks/checkMinimumSize.d.ts +3 -0
  32. package/dist/internals/checks/checkMinimumSize.js +22 -0
  33. package/dist/internals/checks/checkTextInput.d.ts +3 -0
  34. package/dist/internals/checks/checkTextInput.js +51 -0
  35. package/dist/internals/checks/performChecks.d.ts +3 -0
  36. package/dist/internals/checks/performChecks.js +23 -0
  37. package/dist/internals/components/AMAErrorOverlay.d.ts +5 -0
  38. package/dist/internals/components/AMAErrorOverlay.js +334 -0
  39. package/dist/internals/components/AMARuleError.d.ts +7 -0
  40. package/dist/internals/components/AMARuleError.js +161 -0
  41. package/dist/internals/config.d.ts +23 -0
  42. package/dist/internals/config.js +33 -0
  43. package/dist/internals/types.d.ts +9 -0
  44. package/dist/internals/types.js +2 -0
  45. package/dist/internals/useAMADev.d.ts +8 -0
  46. package/dist/internals/useAMADev.dev.d.ts +2 -0
  47. package/dist/internals/useAMADev.dev.js +110 -0
  48. package/dist/internals/useAMADev.js +6 -0
  49. package/dist/internals/useAMADevNodeChecks.dev.d.ts +16 -0
  50. package/dist/internals/useAMADevNodeChecks.dev.js +115 -0
  51. package/dist/internals/useAMADevUiInteraction.dev.d.ts +7 -0
  52. package/dist/internals/useAMADevUiInteraction.dev.js +100 -0
  53. package/dist/internals/utils/amaClearHighlight.d.ts +2 -0
  54. package/dist/internals/utils/amaClearHighlight.js +12 -0
  55. package/dist/internals/utils/amaHighlightComponent.d.ts +3 -0
  56. package/dist/internals/utils/amaHighlightComponent.js +51 -0
  57. package/dist/internals/utils/constants.d.ts +3 -0
  58. package/dist/internals/utils/constants.js +32 -0
  59. package/dist/internals/utils/getErrorColor.d.ts +2 -0
  60. package/dist/internals/utils/getErrorColor.js +10 -0
  61. package/dist/internals/utils/getRuleAction.d.ts +2 -0
  62. package/dist/internals/utils/getRuleAction.js +40 -0
  63. package/dist/internals/utils/getRuleErrorInfo.d.ts +7 -0
  64. package/dist/internals/utils/getRuleErrorInfo.js +19 -0
  65. package/dist/internals/utils/ignoreContrastCheck.d.ts +1 -0
  66. package/dist/internals/utils/ignoreContrastCheck.js +15 -0
  67. package/dist/internals/utils/interpolateAnimationStates.d.ts +2 -0
  68. package/dist/internals/utils/interpolateAnimationStates.js +43 -0
  69. package/dist/internals/utils/isA11yLabelAllowed.d.ts +1 -0
  70. package/dist/internals/utils/isA11yLabelAllowed.js +14 -0
  71. package/dist/internals/utils/isRuleDisabled.d.ts +2 -0
  72. package/dist/internals/utils/isRuleDisabled.js +10 -0
  73. package/dist/internals/utils/logError.d.ts +2 -0
  74. package/dist/internals/utils/logError.js +36 -0
  75. package/dist/internals/utils/logFoundIssues.d.ts +2 -0
  76. package/dist/internals/utils/logFoundIssues.js +23 -0
  77. package/dist/internals/utils/logger.d.ts +6 -0
  78. package/dist/internals/utils/logger.js +15 -0
  79. package/dist/internals/utils/minimumTouchableSize.d.ts +1 -0
  80. package/dist/internals/utils/minimumTouchableSize.js +9 -0
  81. package/dist/internals/utils/platformHelpers.d.ts +2 -0
  82. package/dist/internals/utils/platformHelpers.js +6 -0
  83. package/dist/internals/utils/rules.d.ts +22 -0
  84. package/dist/internals/utils/rules.js +253 -0
  85. package/package.json +61 -18
  86. package/src/ReactNativeAma.types.ts +70 -0
  87. package/src/ReactNativeAmaModule.ts +18 -0
  88. package/src/ReactNativeAmaModule.web.ts +14 -0
  89. package/src/ReactNativeAmaView.tsx +9 -0
  90. package/src/ReactNativeAmaView.web.tsx +15 -0
  91. package/src/__mocks__/ReactNativeAmaModule.ts +9 -0
  92. package/src/components/AMAProvider.integration.test.tsx +3 -96
  93. package/src/components/AMAProvider.test.tsx +9 -4
  94. package/src/components/AMAProvider.tsx +40 -76
  95. package/src/components/AutofocusContainer.test.tsx +4 -5
  96. package/src/components/AutofocusContainer.tsx +20 -5
  97. package/src/components/HideChildrenFromAccessibilityTree.test.tsx +23 -39
  98. package/src/components/HideChildrenFromAccessibilityTree.tsx +12 -11
  99. package/src/hooks/useFocus.test.ts +20 -20
  100. package/src/hooks/useFocus.ts +14 -6
  101. package/src/hooks/useTimedAction.android.test.ts +0 -1
  102. package/src/hooks/useTimedAction.ios.test.ts +0 -1
  103. package/src/hooks/useTimedAction.ts +0 -1
  104. package/src/index.ts +4 -2
  105. package/src/internals/checks/checkAriaLabel.test.ts +165 -0
  106. package/src/internals/checks/checkAriaLabel.ts +16 -0
  107. package/src/internals/checks/checkAriaRole.test.ts +429 -0
  108. package/src/internals/checks/checkAriaRole.ts +60 -0
  109. package/src/internals/checks/checkContrast.test.ts +278 -0
  110. package/src/internals/checks/checkContrast.ts +109 -0
  111. package/src/internals/checks/checkImageAlt.test.ts +125 -0
  112. package/src/internals/checks/checkImageAlt.ts +23 -0
  113. package/src/internals/checks/checkIsUppercase.test.ts +399 -0
  114. package/src/internals/checks/checkIsUppercase.ts +46 -0
  115. package/src/internals/checks/checkLongNumber.test.ts +111 -0
  116. package/src/internals/checks/checkLongNumber.ts +34 -0
  117. package/src/internals/checks/checkMinimumSize.test.ts +67 -0
  118. package/src/internals/checks/checkMinimumSize.ts +25 -0
  119. package/src/internals/checks/checkTextInput.ts +65 -0
  120. package/src/internals/checks/performChecks.test.ts +171 -0
  121. package/src/internals/checks/performChecks.ts +24 -0
  122. package/src/internals/components/AMAErrorOverlay.test.tsx +139 -0
  123. package/src/internals/components/AMAErrorOverlay.tsx +451 -0
  124. package/src/internals/components/AMARuleError.test.tsx +93 -0
  125. package/src/internals/components/AMARuleError.tsx +225 -0
  126. package/src/internals/config.ts +58 -0
  127. package/src/internals/types.ts +11 -0
  128. package/src/internals/useAMADev.dev.ts +153 -0
  129. package/src/internals/useAMADev.test.ts +260 -0
  130. package/src/internals/useAMADev.ts +17 -0
  131. package/src/internals/useAMADevNodeChecks.dev.ts +178 -0
  132. package/src/internals/useAMADevUiInteraction.dev.ts +164 -0
  133. package/src/internals/utils/amaClearHighlight.test.ts +21 -0
  134. package/src/internals/utils/amaClearHighlight.ts +8 -0
  135. package/src/internals/utils/amaHighlightComponent.test.ts +74 -0
  136. package/src/internals/utils/amaHighlightComponent.ts +46 -0
  137. package/src/internals/utils/constants.ts +33 -0
  138. package/src/internals/utils/devMode.test.ts +99 -0
  139. package/src/internals/utils/getErrorColor.test.ts +18 -0
  140. package/src/internals/utils/getErrorColor.ts +8 -0
  141. package/src/internals/utils/getRuleAction.ts +13 -0
  142. package/src/internals/utils/getRuleErrorInfo.test.ts +59 -0
  143. package/src/internals/utils/getRuleErrorInfo.ts +26 -0
  144. package/src/internals/utils/ignoreContrastCheck.test.ts +34 -0
  145. package/src/internals/utils/ignoreContrastCheck.ts +11 -0
  146. package/src/internals/utils/interpolateAnimation.test.ts +176 -0
  147. package/src/internals/utils/interpolateAnimationStates.ts +55 -0
  148. package/src/internals/utils/isA11yLabelAllowed.test.ts +39 -0
  149. package/src/internals/utils/isA11yLabelAllowed.ts +10 -0
  150. package/src/internals/utils/isRuleDisabled.test.ts +35 -0
  151. package/src/internals/utils/isRuleDisabled.ts +10 -0
  152. package/src/internals/utils/logError.test.ts +148 -0
  153. package/src/internals/utils/logError.ts +41 -0
  154. package/src/internals/utils/logFoundIssues.test.ts +98 -0
  155. package/src/internals/utils/logFoundIssues.ts +22 -0
  156. package/src/internals/utils/logger.test.ts +26 -0
  157. package/src/internals/utils/logger.ts +15 -0
  158. package/src/internals/utils/loggerMethods.test.ts +38 -0
  159. package/src/internals/utils/minimumTouchableSize.test.ts +28 -0
  160. package/src/internals/utils/minimumTouchableSize.tsx +9 -0
  161. package/src/internals/utils/platformHelpers.test.ts +11 -0
  162. package/src/internals/utils/platformHelpers.ts +4 -0
  163. package/src/internals/utils/rules.ts +363 -0
  164. package/dist/scripts/create-config.d.ts +0 -1
  165. package/dist/scripts/create-config.js +0 -23
  166. package/dist/src/hooks/useButtonChecks.d.ts +0 -6
  167. package/dist/src/hooks/useButtonChecks.js +0 -51
  168. package/dist/src/hooks/useChecks.d.ts +0 -15
  169. package/dist/src/hooks/useChecks.js +0 -152
  170. package/dist/src/index.js +0 -20
  171. package/src/hooks/useButtonChecks.ts +0 -73
  172. package/src/hooks/useChecks.test.ts +0 -214
  173. package/src/hooks/useChecks.ts +0 -207
  174. /package/dist/{src/components → components}/AutofocusContainer.js +0 -0
  175. /package/dist/{src/hooks → hooks}/useFocus.d.ts +0 -0
  176. /package/dist/{src/hooks → hooks}/useTimedAction.d.ts +0 -0
  177. /package/dist/{src/hooks → hooks}/useTimedAction.js +0 -0
  178. /package/dist/{src/utils → utils}/numerify.d.ts +0 -0
  179. /package/dist/{src/utils → utils}/numerify.js +0 -0
@@ -0,0 +1,165 @@
1
+ import type { AmaNode } from '../../ReactNativeAma.types';
2
+ import { checkAriaLabel } from './checkAriaLabel';
3
+
4
+ describe('checkAriaLabel', () => {
5
+ const createNode = (overrides: Partial<AmaNode> = {}): AmaNode => ({
6
+ type: 'Pressable',
7
+ viewId: 1,
8
+ bounds: [0, 0, 100, 100],
9
+ hasOnSubmitEditing: false,
10
+ ...overrides,
11
+ });
12
+
13
+ describe('Pressable components', () => {
14
+ it('returns error when Pressable has no ariaLabel', () => {
15
+ const node = createNode({
16
+ type: 'Pressable',
17
+ ariaLabel: undefined,
18
+ });
19
+
20
+ const result = checkAriaLabel(node);
21
+ expect(result).toMatchObject({
22
+ rule: 'NO_ACCESSIBILITY_LABEL',
23
+ viewId: 1,
24
+ });
25
+ expect(result?.label).toBeUndefined();
26
+ });
27
+
28
+ it('returns error when Pressable has empty string ariaLabel', () => {
29
+ const node = createNode({
30
+ type: 'Pressable',
31
+ ariaLabel: '',
32
+ });
33
+
34
+ const result = checkAriaLabel(node);
35
+ expect(result).toMatchObject({
36
+ rule: 'NO_ACCESSIBILITY_LABEL',
37
+ viewId: 1,
38
+ });
39
+ });
40
+
41
+ it('returns null when Pressable has valid ariaLabel', () => {
42
+ const node = createNode({
43
+ type: 'Pressable',
44
+ ariaLabel: 'Submit button',
45
+ });
46
+
47
+ const result = checkAriaLabel(node);
48
+ expect(result).toBeNull();
49
+ });
50
+
51
+ it('includes viewId in error', () => {
52
+ const node = createNode({
53
+ type: 'Pressable',
54
+ viewId: 12345,
55
+ ariaLabel: undefined,
56
+ });
57
+
58
+ const result = checkAriaLabel(node);
59
+ expect(result?.viewId).toBe(12345);
60
+ });
61
+ });
62
+
63
+ describe('TextInput components', () => {
64
+ it('returns error when TextInput has no ariaLabel', () => {
65
+ const node = createNode({
66
+ type: 'TextInput',
67
+ ariaLabel: undefined,
68
+ });
69
+
70
+ const result = checkAriaLabel(node);
71
+ expect(result).toMatchObject({
72
+ rule: 'NO_ACCESSIBILITY_LABEL',
73
+ viewId: 1,
74
+ });
75
+ });
76
+
77
+ it('returns error when TextInput has empty string ariaLabel', () => {
78
+ const node = createNode({
79
+ type: 'TextInput',
80
+ ariaLabel: '',
81
+ });
82
+
83
+ const result = checkAriaLabel(node);
84
+ expect(result).toMatchObject({
85
+ rule: 'NO_ACCESSIBILITY_LABEL',
86
+ viewId: 1,
87
+ });
88
+ });
89
+
90
+ it('returns null when TextInput has valid ariaLabel', () => {
91
+ const node = createNode({
92
+ type: 'TextInput',
93
+ ariaLabel: 'Email input',
94
+ });
95
+
96
+ const result = checkAriaLabel(node);
97
+ expect(result).toBeNull();
98
+ });
99
+ });
100
+
101
+ describe('Text components', () => {
102
+ it('returns null when Text has no ariaLabel', () => {
103
+ const node = createNode({
104
+ type: 'Text',
105
+ ariaLabel: undefined,
106
+ });
107
+
108
+ const result = checkAriaLabel(node);
109
+ expect(result).toBeNull();
110
+ });
111
+
112
+ it('returns null when Text has empty string ariaLabel', () => {
113
+ const node = createNode({
114
+ type: 'Text',
115
+ ariaLabel: '',
116
+ });
117
+
118
+ const result = checkAriaLabel(node);
119
+ expect(result).toBeNull();
120
+ });
121
+
122
+ it('returns null when Text has ariaLabel', () => {
123
+ const node = createNode({
124
+ type: 'Text',
125
+ ariaLabel: 'Heading',
126
+ });
127
+
128
+ const result = checkAriaLabel(node);
129
+ expect(result).toBeNull();
130
+ });
131
+ });
132
+
133
+ describe('Edge cases', () => {
134
+ it('treats whitespace-only ariaLabel as missing for Pressable', () => {
135
+ const node = createNode({
136
+ type: 'Pressable',
137
+ ariaLabel: ' ',
138
+ });
139
+
140
+ const result = checkAriaLabel(node);
141
+ // Boolean(' ') is true, so this should pass
142
+ expect(result).toBeNull();
143
+ });
144
+
145
+ it('accepts single character ariaLabel', () => {
146
+ const node = createNode({
147
+ type: 'Pressable',
148
+ ariaLabel: 'X',
149
+ });
150
+
151
+ const result = checkAriaLabel(node);
152
+ expect(result).toBeNull();
153
+ });
154
+
155
+ it('includes the ariaLabel in error when present but empty', () => {
156
+ const node = createNode({
157
+ type: 'Pressable',
158
+ ariaLabel: '',
159
+ });
160
+
161
+ const result = checkAriaLabel(node);
162
+ expect(result?.label).toBe('');
163
+ });
164
+ });
165
+ });
@@ -0,0 +1,16 @@
1
+ import { AmaNode } from '../../ReactNativeAma.types';
2
+ import { AmaError } from '../types';
3
+
4
+ export const checkAriaLabel = (node: AmaNode): AmaError | null => {
5
+ const { ariaLabel } = node;
6
+
7
+ if (['Pressable', 'TextInput'].includes(node.type) && !ariaLabel) {
8
+ return {
9
+ rule: 'NO_ACCESSIBILITY_LABEL',
10
+ label: node.ariaLabel,
11
+ viewId: node.viewId,
12
+ };
13
+ }
14
+
15
+ return null;
16
+ };
@@ -0,0 +1,429 @@
1
+ import { Platform } from 'react-native';
2
+ import type { AmaNode } from '../../ReactNativeAma.types';
3
+ import { checkAriaRole } from './checkAriaRole';
4
+
5
+ describe('checkAriaRole', () => {
6
+ const createNode = (overrides: Partial<AmaNode> = {}): AmaNode => ({
7
+ type: 'Pressable',
8
+ viewId: 1,
9
+ bounds: [0, 0, 100, 100],
10
+ hasOnSubmitEditing: false,
11
+ ...overrides,
12
+ });
13
+
14
+ describe('Platform compatibility', () => {
15
+ it('fails if the role is not supported by the platform (adjustable on Android)', () => {
16
+ Platform.OS = 'android';
17
+
18
+ const node = createNode({
19
+ ariaRole: 'adjustable',
20
+ });
21
+
22
+ const result = checkAriaRole(node);
23
+ expect(result).toMatchObject({
24
+ rule: 'INCOMPATIBLE_ACCESSIBILITY_ROLE',
25
+ extra: '"adjustable" is not a native role for "android"',
26
+ viewId: 1,
27
+ });
28
+ });
29
+
30
+ it('passes for adjustable role on iOS', () => {
31
+ Platform.OS = 'ios';
32
+
33
+ const node = createNode({
34
+ ariaRole: 'adjustable',
35
+ });
36
+
37
+ const result = checkAriaRole(node);
38
+ expect(result).toBeNull();
39
+ });
40
+
41
+ it('fails for checkbox role on iOS', () => {
42
+ Platform.OS = 'ios';
43
+
44
+ const node = createNode({
45
+ ariaRole: 'checkbox',
46
+ });
47
+
48
+ const result = checkAriaRole(node);
49
+ expect(result).toMatchObject({
50
+ rule: 'INCOMPATIBLE_ACCESSIBILITY_ROLE',
51
+ extra: '"checkbox" is not a native role for "ios"',
52
+ });
53
+ });
54
+
55
+ it('passes for checkbox role on Android', () => {
56
+ Platform.OS = 'android';
57
+
58
+ const node = createNode({
59
+ ariaRole: 'checkbox',
60
+ });
61
+
62
+ const result = checkAriaRole(node);
63
+ expect(result).toBeNull();
64
+ });
65
+
66
+ it('fails for togglebutton role on iOS', () => {
67
+ Platform.OS = 'ios';
68
+
69
+ const node = createNode({
70
+ ariaRole: 'togglebutton',
71
+ });
72
+
73
+ const result = checkAriaRole(node);
74
+ expect(result).toMatchObject({
75
+ rule: 'INCOMPATIBLE_ACCESSIBILITY_ROLE',
76
+ extra: '"togglebutton" is not a native role for "ios"',
77
+ });
78
+ });
79
+
80
+ it('passes for togglebutton role on Android', () => {
81
+ Platform.OS = 'android';
82
+
83
+ const node = createNode({
84
+ ariaRole: 'togglebutton',
85
+ });
86
+
87
+ const result = checkAriaRole(node);
88
+ expect(result).toBeNull();
89
+ });
90
+
91
+ it('fails for radio role on iOS', () => {
92
+ Platform.OS = 'ios';
93
+
94
+ const node = createNode({
95
+ ariaRole: 'radio',
96
+ });
97
+
98
+ const result = checkAriaRole(node);
99
+ expect(result).toMatchObject({
100
+ rule: 'INCOMPATIBLE_ACCESSIBILITY_ROLE',
101
+ extra: '"radio" is not a native role for "ios"',
102
+ });
103
+ });
104
+
105
+ it('passes for radio role on Android', () => {
106
+ Platform.OS = 'android';
107
+
108
+ const node = createNode({
109
+ ariaRole: 'radio',
110
+ });
111
+
112
+ const result = checkAriaRole(node);
113
+ expect(result).toBeNull();
114
+ });
115
+ });
116
+
117
+ describe('Cross-platform roles', () => {
118
+ it.each(['ios', 'android'] as const)(
119
+ 'passes for button role on %s',
120
+ (platform) => {
121
+ Platform.OS = platform;
122
+
123
+ const node = createNode({
124
+ ariaRole: 'button',
125
+ });
126
+
127
+ const result = checkAriaRole(node);
128
+ expect(result).toBeNull();
129
+ }
130
+ );
131
+
132
+ it.each(['ios', 'android'] as const)(
133
+ 'passes for switch role on %s',
134
+ (platform) => {
135
+ Platform.OS = platform;
136
+
137
+ const node = createNode({
138
+ ariaRole: 'switch',
139
+ });
140
+
141
+ const result = checkAriaRole(node);
142
+ expect(result).toBeNull();
143
+ }
144
+ );
145
+
146
+ it.each(['ios', 'android'] as const)(
147
+ 'passes for tab role on %s',
148
+ (platform) => {
149
+ Platform.OS = platform;
150
+
151
+ const node = createNode({
152
+ ariaRole: 'tab',
153
+ });
154
+
155
+ const result = checkAriaRole(node);
156
+ expect(result).toBeNull();
157
+ }
158
+ );
159
+
160
+ it.each(['ios', 'android'] as const)(
161
+ 'passes for link role on %s',
162
+ (platform) => {
163
+ Platform.OS = platform;
164
+
165
+ const node = createNode({
166
+ ariaRole: 'link',
167
+ });
168
+
169
+ const result = checkAriaRole(node);
170
+ expect(result).toBeNull();
171
+ }
172
+ );
173
+
174
+ it.each(['ios', 'android'] as const)(
175
+ 'passes for search role on %s',
176
+ (platform) => {
177
+ Platform.OS = platform;
178
+
179
+ const node = createNode({
180
+ ariaRole: 'search',
181
+ });
182
+
183
+ const result = checkAriaRole(node);
184
+ expect(result).toBeNull();
185
+ }
186
+ );
187
+
188
+ it.each(['ios', 'android'] as const)(
189
+ 'passes for header role on %s',
190
+ (platform) => {
191
+ Platform.OS = platform;
192
+
193
+ const node = createNode({
194
+ ariaRole: 'header',
195
+ });
196
+
197
+ const result = checkAriaRole(node);
198
+ expect(result).toBeNull();
199
+ }
200
+ );
201
+
202
+ it.each(['ios', 'android'] as const)(
203
+ 'passes for none role on %s',
204
+ (platform) => {
205
+ Platform.OS = platform;
206
+
207
+ const node = createNode({
208
+ ariaRole: 'none',
209
+ });
210
+
211
+ const result = checkAriaRole(node);
212
+ expect(result).toBeNull();
213
+ }
214
+ );
215
+ });
216
+
217
+ describe('Missing or invalid roles', () => {
218
+ it('fails when Pressable has no ariaRole and no traits', () => {
219
+ Platform.OS = 'android';
220
+
221
+ const node = createNode({
222
+ ariaRole: undefined,
223
+ traits: undefined,
224
+ });
225
+
226
+ const result = checkAriaRole(node);
227
+ expect(result).toMatchObject({
228
+ rule: 'NO_ACCESSIBILITY_ROLE',
229
+ viewId: 1,
230
+ });
231
+ });
232
+
233
+ it('fails when Pressable has empty ariaRole and no traits', () => {
234
+ Platform.OS = 'android';
235
+
236
+ const node = createNode({
237
+ ariaRole: '',
238
+ traits: undefined,
239
+ });
240
+
241
+ const result = checkAriaRole(node);
242
+ expect(result).toMatchObject({
243
+ rule: 'NO_ACCESSIBILITY_ROLE',
244
+ viewId: 1,
245
+ });
246
+ });
247
+
248
+ it('fails when Pressable has empty ariaRole and empty traits', () => {
249
+ Platform.OS = 'android';
250
+
251
+ const node = createNode({
252
+ ariaRole: '',
253
+ traits: [],
254
+ });
255
+
256
+ const result = checkAriaRole(node);
257
+ expect(result).toMatchObject({
258
+ rule: 'NO_ACCESSIBILITY_ROLE',
259
+ viewId: 1,
260
+ });
261
+ });
262
+
263
+ it('fails for unsupported role', () => {
264
+ Platform.OS = 'android';
265
+
266
+ const node = createNode({
267
+ ariaRole: 'unsupported-role',
268
+ });
269
+
270
+ const result = checkAriaRole(node);
271
+ expect(result).toMatchObject({
272
+ rule: 'INCOMPATIBLE_ACCESSIBILITY_ROLE',
273
+ extra: '"unsupported-role" is not a native role for "android"',
274
+ });
275
+ });
276
+ });
277
+
278
+ describe('Traits handling', () => {
279
+ it('uses traits when ariaRole is not provided', () => {
280
+ Platform.OS = 'android';
281
+
282
+ const node = createNode({
283
+ ariaRole: undefined,
284
+ traits: ['button'],
285
+ });
286
+
287
+ const result = checkAriaRole(node);
288
+ expect(result).toBeNull();
289
+ });
290
+
291
+ it('filters out "notEnabled" trait', () => {
292
+ Platform.OS = 'android';
293
+
294
+ const node = createNode({
295
+ ariaRole: undefined,
296
+ traits: ['button', 'notEnabled'],
297
+ });
298
+
299
+ const result = checkAriaRole(node);
300
+ expect(result).toBeNull();
301
+ });
302
+
303
+ it('filters out "selected" trait', () => {
304
+ Platform.OS = 'android';
305
+
306
+ const node = createNode({
307
+ ariaRole: undefined,
308
+ traits: ['button', 'selected'],
309
+ });
310
+
311
+ const result = checkAriaRole(node);
312
+ expect(result).toBeNull();
313
+ });
314
+
315
+ it('filters out both "notEnabled" and "selected" traits', () => {
316
+ Platform.OS = 'android';
317
+
318
+ const node = createNode({
319
+ ariaRole: undefined,
320
+ traits: ['button', 'notEnabled', 'selected'],
321
+ });
322
+
323
+ const result = checkAriaRole(node);
324
+ expect(result).toBeNull();
325
+ });
326
+
327
+ it('joins multiple traits together', () => {
328
+ Platform.OS = 'android';
329
+
330
+ const node = createNode({
331
+ ariaRole: undefined,
332
+ traits: ['toggle', 'button'],
333
+ });
334
+
335
+ const result = checkAriaRole(node);
336
+ // togglebutton is a valid Android role
337
+ expect(result).toBeNull();
338
+ });
339
+
340
+ it('prefers ariaRole over traits when both are present', () => {
341
+ Platform.OS = 'android';
342
+
343
+ const node = createNode({
344
+ ariaRole: 'button',
345
+ traits: ['adjustable'], // Would fail on Android
346
+ });
347
+
348
+ const result = checkAriaRole(node);
349
+ // Should use ariaRole (button) which is valid
350
+ expect(result).toBeNull();
351
+ });
352
+ });
353
+
354
+ describe('Non-Pressable components', () => {
355
+ it('returns null for Text components', () => {
356
+ Platform.OS = 'android';
357
+
358
+ const node = createNode({
359
+ type: 'Text',
360
+ ariaRole: undefined,
361
+ });
362
+
363
+ const result = checkAriaRole(node);
364
+ expect(result).toBeNull();
365
+ });
366
+
367
+ it('returns null for TextInput components', () => {
368
+ Platform.OS = 'android';
369
+
370
+ const node = createNode({
371
+ type: 'TextInput',
372
+ ariaRole: undefined,
373
+ });
374
+
375
+ const result = checkAriaRole(node);
376
+ expect(result).toBeNull();
377
+ });
378
+
379
+ it('returns null for Text components even with invalid role', () => {
380
+ Platform.OS = 'android';
381
+
382
+ const node = createNode({
383
+ type: 'Text',
384
+ ariaRole: 'adjustable', // iOS-only role
385
+ });
386
+
387
+ const result = checkAriaRole(node);
388
+ expect(result).toBeNull();
389
+ });
390
+ });
391
+
392
+ describe('Error structure', () => {
393
+ it('includes ariaLabel in error when present', () => {
394
+ Platform.OS = 'android';
395
+
396
+ const node = createNode({
397
+ ariaRole: undefined,
398
+ ariaLabel: 'Test button',
399
+ });
400
+
401
+ const result = checkAriaRole(node);
402
+ expect(result?.label).toBe('Test button');
403
+ });
404
+
405
+ it('includes viewId in error', () => {
406
+ Platform.OS = 'android';
407
+
408
+ const node = createNode({
409
+ ariaRole: undefined,
410
+ viewId: 12345,
411
+ });
412
+
413
+ const result = checkAriaRole(node);
414
+ expect(result?.viewId).toBe(12345);
415
+ });
416
+
417
+ it('includes platform and role in incompatibility error', () => {
418
+ Platform.OS = 'android';
419
+
420
+ const node = createNode({
421
+ ariaRole: 'adjustable',
422
+ });
423
+
424
+ const result = checkAriaRole(node);
425
+ expect(result?.extra).toContain('adjustable');
426
+ expect(result?.extra).toContain('android');
427
+ });
428
+ });
429
+ });
@@ -0,0 +1,60 @@
1
+ import { Platform } from 'react-native';
2
+ import { AmaNode } from '../../ReactNativeAma.types';
3
+ import { AmaError } from '../types';
4
+
5
+ export const checkAriaRole = (node: AmaNode): AmaError | null => {
6
+ const { ariaRole, traits } = node;
7
+ const filteredTraits = traits?.filter(
8
+ (trait) => trait !== 'notEnabled' && trait !== 'selected'
9
+ );
10
+ const a11yRole = ariaRole ?? filteredTraits?.join('') ?? '';
11
+
12
+ if (node.type !== 'Pressable') {
13
+ return null;
14
+ }
15
+
16
+ if (!a11yRole) {
17
+ return {
18
+ rule: 'NO_ACCESSIBILITY_ROLE',
19
+ label: node.ariaLabel,
20
+ viewId: node.viewId,
21
+ };
22
+ }
23
+
24
+ if (!checkPlatformSupportsRole(a11yRole, Platform.OS)) {
25
+ return {
26
+ label: node.ariaLabel,
27
+ viewId: node.viewId,
28
+ extra: `"${a11yRole}" is not a native role for "${Platform.OS}"`,
29
+ rule: 'INCOMPATIBLE_ACCESSIBILITY_ROLE',
30
+ };
31
+ }
32
+
33
+ return null;
34
+ };
35
+
36
+ const checkPlatformSupportsRole = (
37
+ role: string,
38
+ platform: Partial<typeof Platform.OS>
39
+ ) => {
40
+ const supportedPlatforms = MAPPED_ROLE_CHECKS[role.toLocaleLowerCase()] || [];
41
+
42
+ return supportedPlatforms.includes(platform);
43
+ };
44
+
45
+ const MAPPED_ROLE_CHECKS: {
46
+ [key: string]: Partial<typeof Platform.OS>[];
47
+ } = {
48
+ none: ['ios', 'android'],
49
+ button: ['ios', 'android'],
50
+ switch: ['ios', 'android'],
51
+ checkbox: ['android'],
52
+ tab: ['android', 'ios'],
53
+ togglebutton: ['android'],
54
+ radio: ['android'],
55
+ adjustable: ['ios'],
56
+ link: ['ios', 'android'],
57
+ search: ['ios', 'android'],
58
+ header: ['ios', 'android'],
59
+ image: ['ios', 'android'],
60
+ };