@react-native-ama/core 1.2.1 → 2.0.0-beta.1

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/dist/scripts/create-config.js +4 -3
  86. package/package.json +62 -20
  87. package/src/ReactNativeAma.types.ts +70 -0
  88. package/src/ReactNativeAmaModule.ts +18 -0
  89. package/src/ReactNativeAmaModule.web.ts +14 -0
  90. package/src/ReactNativeAmaView.tsx +9 -0
  91. package/src/ReactNativeAmaView.web.tsx +15 -0
  92. package/src/__mocks__/ReactNativeAmaModule.ts +9 -0
  93. package/src/components/AMAProvider.integration.test.tsx +3 -96
  94. package/src/components/AMAProvider.test.tsx +9 -4
  95. package/src/components/AMAProvider.tsx +40 -76
  96. package/src/components/AutofocusContainer.test.tsx +4 -5
  97. package/src/components/AutofocusContainer.tsx +20 -5
  98. package/src/components/HideChildrenFromAccessibilityTree.test.tsx +23 -39
  99. package/src/components/HideChildrenFromAccessibilityTree.tsx +12 -11
  100. package/src/hooks/useFocus.test.ts +20 -20
  101. package/src/hooks/useFocus.ts +14 -6
  102. package/src/hooks/useTimedAction.android.test.ts +0 -1
  103. package/src/hooks/useTimedAction.ios.test.ts +0 -1
  104. package/src/hooks/useTimedAction.ts +0 -1
  105. package/src/index.ts +4 -2
  106. package/src/internals/checks/checkAriaLabel.test.ts +165 -0
  107. package/src/internals/checks/checkAriaLabel.ts +16 -0
  108. package/src/internals/checks/checkAriaRole.test.ts +429 -0
  109. package/src/internals/checks/checkAriaRole.ts +60 -0
  110. package/src/internals/checks/checkContrast.test.ts +278 -0
  111. package/src/internals/checks/checkContrast.ts +109 -0
  112. package/src/internals/checks/checkImageAlt.test.ts +125 -0
  113. package/src/internals/checks/checkImageAlt.ts +23 -0
  114. package/src/internals/checks/checkIsUppercase.test.ts +399 -0
  115. package/src/internals/checks/checkIsUppercase.ts +46 -0
  116. package/src/internals/checks/checkLongNumber.test.ts +111 -0
  117. package/src/internals/checks/checkLongNumber.ts +34 -0
  118. package/src/internals/checks/checkMinimumSize.test.ts +67 -0
  119. package/src/internals/checks/checkMinimumSize.ts +25 -0
  120. package/src/internals/checks/checkTextInput.ts +65 -0
  121. package/src/internals/checks/performChecks.test.ts +171 -0
  122. package/src/internals/checks/performChecks.ts +24 -0
  123. package/src/internals/components/AMAErrorOverlay.test.tsx +139 -0
  124. package/src/internals/components/AMAErrorOverlay.tsx +451 -0
  125. package/src/internals/components/AMARuleError.test.tsx +93 -0
  126. package/src/internals/components/AMARuleError.tsx +225 -0
  127. package/src/internals/config.ts +58 -0
  128. package/src/internals/types.ts +11 -0
  129. package/src/internals/useAMADev.dev.ts +153 -0
  130. package/src/internals/useAMADev.test.ts +260 -0
  131. package/src/internals/useAMADev.ts +17 -0
  132. package/src/internals/useAMADevNodeChecks.dev.ts +178 -0
  133. package/src/internals/useAMADevUiInteraction.dev.ts +164 -0
  134. package/src/internals/utils/amaClearHighlight.test.ts +21 -0
  135. package/src/internals/utils/amaClearHighlight.ts +8 -0
  136. package/src/internals/utils/amaHighlightComponent.test.ts +74 -0
  137. package/src/internals/utils/amaHighlightComponent.ts +46 -0
  138. package/src/internals/utils/constants.ts +33 -0
  139. package/src/internals/utils/devMode.test.ts +99 -0
  140. package/src/internals/utils/getErrorColor.test.ts +18 -0
  141. package/src/internals/utils/getErrorColor.ts +8 -0
  142. package/src/internals/utils/getRuleAction.ts +13 -0
  143. package/src/internals/utils/getRuleErrorInfo.test.ts +59 -0
  144. package/src/internals/utils/getRuleErrorInfo.ts +26 -0
  145. package/src/internals/utils/ignoreContrastCheck.test.ts +34 -0
  146. package/src/internals/utils/ignoreContrastCheck.ts +11 -0
  147. package/src/internals/utils/interpolateAnimation.test.ts +176 -0
  148. package/src/internals/utils/interpolateAnimationStates.ts +55 -0
  149. package/src/internals/utils/isA11yLabelAllowed.test.ts +39 -0
  150. package/src/internals/utils/isA11yLabelAllowed.ts +10 -0
  151. package/src/internals/utils/isRuleDisabled.test.ts +35 -0
  152. package/src/internals/utils/isRuleDisabled.ts +10 -0
  153. package/src/internals/utils/logError.test.ts +148 -0
  154. package/src/internals/utils/logError.ts +41 -0
  155. package/src/internals/utils/logFoundIssues.test.ts +98 -0
  156. package/src/internals/utils/logFoundIssues.ts +22 -0
  157. package/src/internals/utils/logger.test.ts +26 -0
  158. package/src/internals/utils/logger.ts +15 -0
  159. package/src/internals/utils/loggerMethods.test.ts +38 -0
  160. package/src/internals/utils/minimumTouchableSize.test.ts +28 -0
  161. package/src/internals/utils/minimumTouchableSize.tsx +9 -0
  162. package/src/internals/utils/platformHelpers.test.ts +11 -0
  163. package/src/internals/utils/platformHelpers.ts +4 -0
  164. package/src/internals/utils/rules.ts +363 -0
  165. package/src/scripts/create-config.ts +21 -0
  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,399 @@
1
+ import type { AmaNode } from '../../ReactNativeAma.types';
2
+ import type { AmaRule } from '../types';
3
+ import { checkIsUppercase } from './checkIsUppercase';
4
+
5
+ jest.mock('../utils/isA11yLabelAllowed', () => ({
6
+ isAccessibilityLabelAllowed: jest.fn(),
7
+ }));
8
+
9
+ describe('checkIsUppercase', () => {
10
+ const createNode = (overrides: Partial<AmaNode> = {}): AmaNode => ({
11
+ type: 'Text',
12
+ viewId: 1,
13
+ bounds: [0, 0, 100, 100],
14
+ hasOnSubmitEditing: false,
15
+ isAccessible: true,
16
+ ...overrides,
17
+ });
18
+
19
+ beforeEach(() => {
20
+ jest.clearAllMocks();
21
+ });
22
+
23
+ describe('Uppercase detection', () => {
24
+ it('returns error when text is all uppercase', () => {
25
+ const { isAccessibilityLabelAllowed } = require('../utils/isA11yLabelAllowed');
26
+ isAccessibilityLabelAllowed.mockReturnValue(false);
27
+
28
+ const node = createNode({
29
+ content: 'HELLO WORLD',
30
+ });
31
+
32
+ const result = checkIsUppercase({ node });
33
+ expect(result).toMatchObject({
34
+ rule: 'NO_UPPERCASE_TEXT',
35
+ viewId: 1,
36
+ });
37
+ });
38
+
39
+ it('returns null when text is lowercase', () => {
40
+ const { isAccessibilityLabelAllowed } = require('../utils/isA11yLabelAllowed');
41
+ isAccessibilityLabelAllowed.mockReturnValue(false);
42
+
43
+ const node = createNode({
44
+ content: 'hello world',
45
+ });
46
+
47
+ const result = checkIsUppercase({ node });
48
+ expect(result).toBeNull();
49
+ });
50
+
51
+ it('returns null when text is mixed case', () => {
52
+ const { isAccessibilityLabelAllowed } = require('../utils/isA11yLabelAllowed');
53
+ isAccessibilityLabelAllowed.mockReturnValue(false);
54
+
55
+ const node = createNode({
56
+ content: 'Hello World',
57
+ });
58
+
59
+ const result = checkIsUppercase({ node });
60
+ expect(result).toBeNull();
61
+ });
62
+
63
+ it('returns error for single uppercase letter', () => {
64
+ const { isAccessibilityLabelAllowed } = require('../utils/isA11yLabelAllowed');
65
+ isAccessibilityLabelAllowed.mockReturnValue(false);
66
+
67
+ const node = createNode({
68
+ content: 'A',
69
+ });
70
+
71
+ const result = checkIsUppercase({ node });
72
+ expect(result).toMatchObject({
73
+ rule: 'NO_UPPERCASE_TEXT',
74
+ });
75
+ });
76
+
77
+ it('returns null for single lowercase letter', () => {
78
+ const { isAccessibilityLabelAllowed } = require('../utils/isA11yLabelAllowed');
79
+ isAccessibilityLabelAllowed.mockReturnValue(false);
80
+
81
+ const node = createNode({
82
+ content: 'a',
83
+ });
84
+
85
+ const result = checkIsUppercase({ node });
86
+ expect(result).toBeNull();
87
+ });
88
+
89
+ it('returns error for numbers and uppercase letters', () => {
90
+ const { isAccessibilityLabelAllowed } = require('../utils/isA11yLabelAllowed');
91
+ isAccessibilityLabelAllowed.mockReturnValue(false);
92
+
93
+ const node = createNode({
94
+ content: 'HELLO123',
95
+ });
96
+
97
+ const result = checkIsUppercase({ node });
98
+ expect(result).toMatchObject({
99
+ rule: 'NO_UPPERCASE_TEXT',
100
+ });
101
+ });
102
+
103
+ it('returns error for symbols and uppercase letters', () => {
104
+ const { isAccessibilityLabelAllowed } = require('../utils/isA11yLabelAllowed');
105
+ isAccessibilityLabelAllowed.mockReturnValue(false);
106
+
107
+ const node = createNode({
108
+ content: 'HELLO!',
109
+ });
110
+
111
+ const result = checkIsUppercase({ node });
112
+ expect(result).toMatchObject({
113
+ rule: 'NO_UPPERCASE_TEXT',
114
+ });
115
+ });
116
+
117
+ it('returns null for numbers only', () => {
118
+ const { isAccessibilityLabelAllowed } = require('../utils/isA11yLabelAllowed');
119
+ isAccessibilityLabelAllowed.mockReturnValue(false);
120
+
121
+ const node = createNode({
122
+ content: '12345',
123
+ });
124
+
125
+ const result = checkIsUppercase({ node });
126
+ expect(result).toBeNull();
127
+ });
128
+
129
+ it('returns null for symbols only', () => {
130
+ const { isAccessibilityLabelAllowed } = require('../utils/isA11yLabelAllowed');
131
+ isAccessibilityLabelAllowed.mockReturnValue(false);
132
+
133
+ const node = createNode({
134
+ content: '!!!',
135
+ });
136
+
137
+ const result = checkIsUppercase({ node });
138
+ expect(result).toBeNull();
139
+ });
140
+ });
141
+
142
+ describe('AriaLabel vs Content priority', () => {
143
+ it('prefers ariaLabel over content when both are present', () => {
144
+ const { isAccessibilityLabelAllowed } = require('../utils/isA11yLabelAllowed');
145
+ isAccessibilityLabelAllowed.mockReturnValue(false);
146
+
147
+ const node = createNode({
148
+ ariaLabel: 'UPPERCASE LABEL',
149
+ content: 'lowercase content',
150
+ });
151
+
152
+ const result = checkIsUppercase({ node });
153
+ expect(result).toMatchObject({
154
+ rule: 'NO_UPPERCASE_TEXT',
155
+ });
156
+ });
157
+
158
+ it('uses content when ariaLabel is not provided', () => {
159
+ const { isAccessibilityLabelAllowed } = require('../utils/isA11yLabelAllowed');
160
+ isAccessibilityLabelAllowed.mockReturnValue(false);
161
+
162
+ const node = createNode({
163
+ ariaLabel: undefined,
164
+ content: 'UPPERCASE CONTENT',
165
+ });
166
+
167
+ const result = checkIsUppercase({ node });
168
+ expect(result).toMatchObject({
169
+ rule: 'NO_UPPERCASE_TEXT',
170
+ });
171
+ });
172
+
173
+ it('returns null when ariaLabel is lowercase and content is uppercase', () => {
174
+ const { isAccessibilityLabelAllowed } = require('../utils/isA11yLabelAllowed');
175
+ isAccessibilityLabelAllowed.mockReturnValue(false);
176
+
177
+ const node = createNode({
178
+ ariaLabel: 'lowercase label',
179
+ content: 'UPPERCASE CONTENT',
180
+ });
181
+
182
+ const result = checkIsUppercase({ node });
183
+ expect(result).toBeNull();
184
+ });
185
+ });
186
+
187
+ describe('Empty text handling', () => {
188
+ it('returns null for empty content when canBeEmpty is true (default)', () => {
189
+ const node = createNode({
190
+ content: '',
191
+ });
192
+
193
+ const result = checkIsUppercase({ node });
194
+ expect(result).toBeNull();
195
+ });
196
+
197
+ it('returns null for undefined content when canBeEmpty is true', () => {
198
+ const node = createNode({
199
+ content: undefined,
200
+ });
201
+
202
+ const result = checkIsUppercase({ node });
203
+ expect(result).toBeNull();
204
+ });
205
+
206
+ it('returns null for whitespace-only content when canBeEmpty is true', () => {
207
+ const node = createNode({
208
+ content: ' ',
209
+ });
210
+
211
+ const result = checkIsUppercase({ node });
212
+ expect(result).toBeNull();
213
+ });
214
+
215
+ it('returns null for empty content when canBeEmpty is false', () => {
216
+ const node = createNode({
217
+ content: '',
218
+ });
219
+
220
+ const result = checkIsUppercase({ node, canBeEmpty: false });
221
+ expect(result).toBeNull();
222
+ });
223
+
224
+ it('returns null for whitespace-only content when canBeEmpty is false', () => {
225
+ const node = createNode({
226
+ content: ' ',
227
+ });
228
+
229
+ const result = checkIsUppercase({ node, canBeEmpty: false });
230
+ expect(result).toBeNull();
231
+ });
232
+ });
233
+
234
+ describe('Allowed exceptions', () => {
235
+ it('returns null when text is allowed by isAccessibilityLabelAllowed', () => {
236
+ const { isAccessibilityLabelAllowed } = require('../utils/isA11yLabelAllowed');
237
+ isAccessibilityLabelAllowed.mockReturnValue(true);
238
+
239
+ const node = createNode({
240
+ content: 'ALLOWED EXCEPTION',
241
+ });
242
+
243
+ const result = checkIsUppercase({ node });
244
+ expect(result).toBeNull();
245
+ });
246
+
247
+ it('calls isAccessibilityLabelAllowed with the correct text', () => {
248
+ const { isAccessibilityLabelAllowed } = require('../utils/isA11yLabelAllowed');
249
+ isAccessibilityLabelAllowed.mockReturnValue(false);
250
+
251
+ const node = createNode({
252
+ content: 'TEST TEXT',
253
+ });
254
+
255
+ checkIsUppercase({ node });
256
+ expect(isAccessibilityLabelAllowed).toHaveBeenCalledWith('TEST TEXT');
257
+ });
258
+
259
+ it('calls isAccessibilityLabelAllowed with ariaLabel when present', () => {
260
+ const { isAccessibilityLabelAllowed } = require('../utils/isA11yLabelAllowed');
261
+ isAccessibilityLabelAllowed.mockReturnValue(false);
262
+
263
+ const node = createNode({
264
+ ariaLabel: 'ARIA LABEL',
265
+ content: 'content',
266
+ });
267
+
268
+ checkIsUppercase({ node });
269
+ expect(isAccessibilityLabelAllowed).toHaveBeenCalledWith('ARIA LABEL');
270
+ });
271
+ });
272
+
273
+ describe('Custom rule parameter', () => {
274
+ it('uses custom rule when provided', () => {
275
+ const { isAccessibilityLabelAllowed } = require('../utils/isA11yLabelAllowed');
276
+ isAccessibilityLabelAllowed.mockReturnValue(false);
277
+
278
+ const node = createNode({
279
+ content: 'UPPERCASE',
280
+ });
281
+
282
+ const result = checkIsUppercase({ node, rule: 'CUSTOM_UPPERCASE_RULE' as AmaRule });
283
+ expect(result).toMatchObject({
284
+ rule: 'CUSTOM_UPPERCASE_RULE',
285
+ });
286
+ });
287
+
288
+ it('uses default rule when not provided', () => {
289
+ const { isAccessibilityLabelAllowed } = require('../utils/isA11yLabelAllowed');
290
+ isAccessibilityLabelAllowed.mockReturnValue(false);
291
+
292
+ const node = createNode({
293
+ content: 'UPPERCASE',
294
+ });
295
+
296
+ const result = checkIsUppercase({ node });
297
+ expect(result).toMatchObject({
298
+ rule: 'NO_UPPERCASE_TEXT',
299
+ });
300
+ });
301
+ });
302
+
303
+ describe('Error structure', () => {
304
+ it('includes ariaLabel in error when present', () => {
305
+ const { isAccessibilityLabelAllowed } = require('../utils/isA11yLabelAllowed');
306
+ isAccessibilityLabelAllowed.mockReturnValue(false);
307
+
308
+ const node = createNode({
309
+ content: 'lowercase',
310
+ ariaLabel: 'BUTTON LABEL',
311
+ });
312
+
313
+ const result = checkIsUppercase({ node });
314
+ expect(result?.label).toBe('BUTTON LABEL');
315
+ });
316
+
317
+ it('includes viewId in error', () => {
318
+ const { isAccessibilityLabelAllowed } = require('../utils/isA11yLabelAllowed');
319
+ isAccessibilityLabelAllowed.mockReturnValue(false);
320
+
321
+ const node = createNode({
322
+ content: 'UPPERCASE',
323
+ viewId: 12345,
324
+ });
325
+
326
+ const result = checkIsUppercase({ node });
327
+ expect(result?.viewId).toBe(12345);
328
+ });
329
+
330
+ it('includes undefined ariaLabel when not present', () => {
331
+ const { isAccessibilityLabelAllowed } = require('../utils/isA11yLabelAllowed');
332
+ isAccessibilityLabelAllowed.mockReturnValue(false);
333
+
334
+ const node = createNode({
335
+ content: 'UPPERCASE',
336
+ ariaLabel: undefined,
337
+ });
338
+
339
+ const result = checkIsUppercase({ node });
340
+ expect(result?.label).toBeUndefined();
341
+ });
342
+ });
343
+
344
+ describe('Edge cases', () => {
345
+ it('returns error for uppercase with leading/trailing whitespace', () => {
346
+ const { isAccessibilityLabelAllowed } = require('../utils/isA11yLabelAllowed');
347
+ isAccessibilityLabelAllowed.mockReturnValue(false);
348
+
349
+ const node = createNode({
350
+ content: ' UPPERCASE ',
351
+ });
352
+
353
+ const result = checkIsUppercase({ node });
354
+ expect(result).toMatchObject({
355
+ rule: 'NO_UPPERCASE_TEXT',
356
+ });
357
+ });
358
+
359
+ it('handles unicode uppercase characters', () => {
360
+ const { isAccessibilityLabelAllowed } = require('../utils/isA11yLabelAllowed');
361
+ isAccessibilityLabelAllowed.mockReturnValue(false);
362
+
363
+ const node = createNode({
364
+ content: 'ÀÉÎÖÜ',
365
+ });
366
+
367
+ const result = checkIsUppercase({ node });
368
+ expect(result).toMatchObject({
369
+ rule: 'NO_UPPERCASE_TEXT',
370
+ });
371
+ });
372
+
373
+ it('returns null for unicode lowercase characters', () => {
374
+ const { isAccessibilityLabelAllowed } = require('../utils/isA11yLabelAllowed');
375
+ isAccessibilityLabelAllowed.mockReturnValue(false);
376
+
377
+ const node = createNode({
378
+ content: 'àéîöü',
379
+ });
380
+
381
+ const result = checkIsUppercase({ node });
382
+ expect(result).toBeNull();
383
+ });
384
+
385
+ it('handles newlines in uppercase text', () => {
386
+ const { isAccessibilityLabelAllowed } = require('../utils/isA11yLabelAllowed');
387
+ isAccessibilityLabelAllowed.mockReturnValue(false);
388
+
389
+ const node = createNode({
390
+ content: 'HELLO\nWORLD',
391
+ });
392
+
393
+ const result = checkIsUppercase({ node });
394
+ expect(result).toMatchObject({
395
+ rule: 'NO_UPPERCASE_TEXT',
396
+ });
397
+ });
398
+ });
399
+ });
@@ -0,0 +1,46 @@
1
+ import { AmaNode } from '../../ReactNativeAma.types';
2
+ import { AmaError, AmaRule } from '../types';
3
+ import { isAccessibilityLabelAllowed } from '../utils/isA11yLabelAllowed';
4
+
5
+ export type UppercaseStringChecker = {
6
+ node: AmaNode;
7
+ canBeEmpty?: boolean;
8
+ rule?: AmaRule;
9
+ };
10
+
11
+ export const checkIsUppercase = ({
12
+ node,
13
+ canBeEmpty = true,
14
+ rule = 'NO_UPPERCASE_TEXT',
15
+ }: UppercaseStringChecker): AmaError | null => {
16
+ const text = node.ariaLabel ?? node.content;
17
+ const isEmpty = !text || text?.trim()?.length === 0;
18
+
19
+ if ((isEmpty && canBeEmpty) || !node.isAccessible) {
20
+ return null;
21
+ }
22
+
23
+ const isAllowed = isAccessibilityLabelAllowed?.(text!);
24
+
25
+ if (!isAllowed && isUpperCase(text!)) {
26
+ return {
27
+ rule,
28
+ label: node.ariaLabel,
29
+ viewId: node.viewId,
30
+ };
31
+ }
32
+
33
+ return null;
34
+ };
35
+
36
+ const NO_LETTERS = /[^\p{L}]/gu;
37
+
38
+ const isUpperCase = (string: string) => {
39
+ const cleanString = string.replace(NO_LETTERS, '');
40
+
41
+ if (cleanString.trim().length === 0) {
42
+ return false;
43
+ }
44
+
45
+ return cleanString.toUpperCase() === cleanString;
46
+ };
@@ -0,0 +1,111 @@
1
+ import type { AmaNode } from '../../ReactNativeAma.types';
2
+
3
+ beforeEach(() => {
4
+ jest.resetModules();
5
+ jest.clearAllMocks();
6
+ // @ts-ignore
7
+ global.__DEV__ = true;
8
+ });
9
+
10
+ const createNode = (overrides: Partial<AmaNode> = {}): AmaNode => ({
11
+ type: 'Text',
12
+ viewId: 1,
13
+ bounds: [0, 0, 100, 100],
14
+ hasOnSubmitEditing: false,
15
+ isAccessible: true,
16
+ ...overrides,
17
+ });
18
+
19
+ const setupMock = (longNumberMinLength = 12) => {
20
+ jest.doMock('../config', () => ({
21
+ __esModule: true,
22
+ default: { longNumberMinLength },
23
+ }));
24
+ };
25
+
26
+ describe('checkLongNumber', () => {
27
+ it('flags text with an unformatted digit run at or above the threshold', () => {
28
+ setupMock(12);
29
+ const { checkLongNumber } = require('./checkLongNumber');
30
+
31
+ const node = createNode({ content: '4111111111111111' });
32
+
33
+ const result = checkLongNumber(node);
34
+ expect(result).toMatchObject({
35
+ rule: 'LONG_NUMBER_NOT_FORMATTED',
36
+ viewId: 1,
37
+ });
38
+ });
39
+
40
+ it('does not flag a formatted phone number, even with more digits than the threshold', () => {
41
+ setupMock(12);
42
+ const { checkLongNumber } = require('./checkLongNumber');
43
+
44
+ const node = createNode({ content: '+44 20 7123 4567' });
45
+
46
+ const result = checkLongNumber(node);
47
+ expect(result).toBeNull();
48
+ });
49
+
50
+ it('does not flag a digit run shorter than the threshold', () => {
51
+ setupMock(12);
52
+ const { checkLongNumber } = require('./checkLongNumber');
53
+
54
+ const node = createNode({ content: '1234567890' });
55
+
56
+ const result = checkLongNumber(node);
57
+ expect(result).toBeNull();
58
+ });
59
+
60
+ it('flags a shorter digit run when longNumberMinLength is lowered', () => {
61
+ setupMock(8);
62
+ const { checkLongNumber } = require('./checkLongNumber');
63
+
64
+ const node = createNode({ content: '12345678' });
65
+
66
+ const result = checkLongNumber(node);
67
+ expect(result).toMatchObject({
68
+ rule: 'LONG_NUMBER_NOT_FORMATTED',
69
+ viewId: 1,
70
+ });
71
+ });
72
+
73
+ it('prefers ariaLabel over content when checking for a long number', () => {
74
+ setupMock(12);
75
+ const { checkLongNumber } = require('./checkLongNumber');
76
+
77
+ const node = createNode({
78
+ ariaLabel: '4111111111111111',
79
+ content: 'Card number',
80
+ });
81
+
82
+ const result = checkLongNumber(node);
83
+ expect(result).toMatchObject({
84
+ rule: 'LONG_NUMBER_NOT_FORMATTED',
85
+ label: '4111111111111111',
86
+ });
87
+ });
88
+
89
+ it('returns null when there is no text content', () => {
90
+ setupMock(12);
91
+ const { checkLongNumber } = require('./checkLongNumber');
92
+
93
+ const node = createNode({ content: undefined, ariaLabel: undefined });
94
+
95
+ const result = checkLongNumber(node);
96
+ expect(result).toBeNull();
97
+ });
98
+
99
+ it('returns null when the node is not accessible', () => {
100
+ setupMock(12);
101
+ const { checkLongNumber } = require('./checkLongNumber');
102
+
103
+ const node = createNode({
104
+ content: '4111111111111111',
105
+ isAccessible: false,
106
+ });
107
+
108
+ const result = checkLongNumber(node);
109
+ expect(result).toBeNull();
110
+ });
111
+ });
@@ -0,0 +1,34 @@
1
+ import { AmaNode } from '../../ReactNativeAma.types';
2
+ import projectRules from '../config';
3
+ import { AmaError } from '../types';
4
+
5
+ export const checkLongNumber = (node: AmaNode): AmaError | null => {
6
+ const text = node.ariaLabel ?? node.content;
7
+
8
+ if (!text || !node.isAccessible) {
9
+ return null;
10
+ }
11
+
12
+ const hasFailed =
13
+ getLongestDigitRun(text) >= projectRules.longNumberMinLength;
14
+
15
+ if (hasFailed) {
16
+ return {
17
+ rule: 'LONG_NUMBER_NOT_FORMATTED',
18
+ label: node.ariaLabel,
19
+ viewId: node.viewId,
20
+ };
21
+ }
22
+
23
+ return null;
24
+ };
25
+
26
+ const getLongestDigitRun = (text: string): number => {
27
+ const digitRuns = text.match(/\d+/g);
28
+
29
+ if (!digitRuns) {
30
+ return 0;
31
+ }
32
+
33
+ return Math.max(...digitRuns.map((run) => run.length));
34
+ };
@@ -0,0 +1,67 @@
1
+ import { AmaNode } from '../../ReactNativeAma.types';
2
+
3
+ jest.mock('react-native', () => ({ Platform: { OS: 'ios' } }));
4
+
5
+ beforeEach(() => {
6
+ jest.clearAllMocks();
7
+ });
8
+
9
+ const createNode = (overrides: Partial<AmaNode> = {}): AmaNode => ({
10
+ type: 'Pressable',
11
+ viewId: 1,
12
+ bounds: [44, 44, 0, 0],
13
+ hasOnSubmitEditing: false,
14
+ isAccessible: true,
15
+ ...overrides,
16
+ });
17
+
18
+ describe('checkMinimumSize', () => {
19
+ it.each`
20
+ platform | minimumSize | layoutSize
21
+ ${'android'} | ${48} | ${45}
22
+ ${'ios'} | ${44} | ${43}
23
+ `(
24
+ '$platform: returns MINIMUM_SIZE error if touchable area is smaller than required',
25
+ ({ platform, minimumSize, layoutSize }) => {
26
+ jest.resetModules();
27
+ jest.doMock('react-native', () => ({ Platform: { OS: platform } }));
28
+
29
+ const { checkMinimumSize } = require('./checkMinimumSize');
30
+
31
+ const result = checkMinimumSize(
32
+ createNode({ bounds: [layoutSize, layoutSize, 0, 0] }),
33
+ );
34
+
35
+ expect(result).toMatchObject({
36
+ rule: 'MINIMUM_SIZE',
37
+ extra: expect.stringContaining(
38
+ `${minimumSize}x${minimumSize} found instead: ${layoutSize}x${layoutSize}`,
39
+ ),
40
+ });
41
+ },
42
+ );
43
+
44
+ it('returns null if touchable area meets minimum size', () => {
45
+ jest.resetModules();
46
+ jest.doMock('react-native', () => ({ Platform: { OS: 'ios' } }));
47
+
48
+ const { checkMinimumSize } = require('./checkMinimumSize');
49
+
50
+ const result = checkMinimumSize(createNode({ bounds: [44, 44, 0, 0] }));
51
+
52
+ expect(result).toBeNull();
53
+ });
54
+
55
+ it('returns null for non-Pressable nodes', () => {
56
+ jest.resetModules();
57
+ jest.doMock('react-native', () => ({ Platform: { OS: 'ios' } }));
58
+
59
+ const { checkMinimumSize } = require('./checkMinimumSize');
60
+
61
+ const result = checkMinimumSize(
62
+ createNode({ type: 'Text', bounds: [10, 10, 0, 0] }),
63
+ );
64
+
65
+ expect(result).toBeNull();
66
+ });
67
+ });
@@ -0,0 +1,25 @@
1
+ import { AmaNode } from '../../ReactNativeAma.types';
2
+ import { AmaError } from '../types';
3
+ import { MINIMUM_TOUCHABLE_SIZE } from '../utils/minimumTouchableSize';
4
+
5
+ export const checkMinimumSize = (node: AmaNode): AmaError | null => {
6
+ const width = Math.ceil(node.bounds?.[0]);
7
+ const height = Math.ceil(node.bounds?.[1]);
8
+
9
+ if (node.type !== 'Pressable') {
10
+ return null;
11
+ }
12
+
13
+ if (width < MINIMUM_TOUCHABLE_SIZE || height < MINIMUM_TOUCHABLE_SIZE) {
14
+ return {
15
+ label: node.ariaLabel,
16
+ viewId: node.viewId,
17
+ rule: 'MINIMUM_SIZE',
18
+ extra: `The touchable area must have a minimum size of ${MINIMUM_TOUCHABLE_SIZE}x${MINIMUM_TOUCHABLE_SIZE} found instead: ${width.toFixed(
19
+ 0
20
+ )}x${height.toFixed(0)}`,
21
+ };
22
+ }
23
+
24
+ return null;
25
+ };