@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,225 @@
1
+ /* eslint-disable react-native/no-inline-styles */
2
+ import React, { ReactNode } from 'react';
3
+ import { Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
4
+ import { AmaError } from '../types';
5
+ import { A11ySeverity } from '../utils/getRuleErrorInfo';
6
+ import { RULES_HELP } from '../utils/rules';
7
+
8
+ const SEVERITIES: { [key in A11ySeverity]: string } | null = __DEV__
9
+ ? {
10
+ Serious: 'Serious',
11
+ Critical: 'Critical',
12
+ Warning: 'Warning',
13
+ }
14
+ : null;
15
+
16
+ const REGEX_MARKDOWN = /(`[^`]+`|\*\*[^*]+\*\*)/g;
17
+
18
+ export const AMARuleError = __DEV__
19
+ ? ({
20
+ issue,
21
+ maxHeight,
22
+ onClose,
23
+ }: {
24
+ issue: AmaError;
25
+ onClose: () => void;
26
+ maxHeight: number;
27
+ }) => {
28
+ const ruleHelp = RULES_HELP![issue.rule];
29
+ const COLOR_TOKEN_REGEX = /(\(#[0-9A-Fa-f]{3,8}\))/g;
30
+
31
+ const renderMarkdown = (text: string) => {
32
+ const parts = text.split(REGEX_MARKDOWN);
33
+
34
+ return parts.map((part, index) => {
35
+ if (part.startsWith('`') && part.endsWith('`') && Code) {
36
+ return <Code key={index}>{part.slice(1, -1)}</Code>;
37
+ }
38
+
39
+ if (part.startsWith('**') && part.endsWith('**')) {
40
+ return (
41
+ <Text key={index} style={[styles?.text, styles?.bold]}>
42
+ {part.slice(2, -2)}
43
+ </Text>
44
+ );
45
+ }
46
+
47
+ return (
48
+ <Text key={index} style={styles?.text}>
49
+ {part}
50
+ </Text>
51
+ );
52
+ });
53
+ };
54
+
55
+ const renderHowToFix = (text: string | string[]) => {
56
+ if (typeof text === 'string') {
57
+ return <Text style={styles?.text}>{renderMarkdown(text)}</Text>;
58
+ }
59
+
60
+ return (
61
+ <View>
62
+ {text.map((value, index) => {
63
+ return (
64
+ <Text key={index} style={styles!.text}>
65
+ {renderMarkdown(value)}
66
+ </Text>
67
+ );
68
+ })}
69
+ </View>
70
+ );
71
+ };
72
+
73
+ const renderTextWithColorSwatches = (text: string) => {
74
+ const parts = text.split(COLOR_TOKEN_REGEX);
75
+
76
+ return parts.map((part, index) => {
77
+ if (part.startsWith('(#') && part.endsWith(')')) {
78
+ const color = part.slice(1, -1);
79
+
80
+ return (
81
+ <React.Fragment key={index}>
82
+ <Text
83
+ style={styles?.text}
84
+ importantForAccessibility="no-hide-descendants"
85
+ accessibilityElementsHidden
86
+ >
87
+ (
88
+ </Text>
89
+ <Text
90
+ style={styles!.swatchFrame}
91
+ importantForAccessibility="no-hide-descendants"
92
+ accessibilityElementsHidden
93
+ >
94
+ <Text
95
+ style={[styles!.swatchGlyph, { color }]}
96
+ importantForAccessibility="no-hide-descendants"
97
+ accessibilityElementsHidden
98
+ >
99
+
100
+ </Text>
101
+ </Text>
102
+ <Text
103
+ style={styles?.text}
104
+ importantForAccessibility="no-hide-descendants"
105
+ accessibilityElementsHidden
106
+ accessible={false}
107
+ >
108
+ {` ${color}`}
109
+ </Text>
110
+ <Text
111
+ style={styles?.text}
112
+ importantForAccessibility="no-hide-descendants"
113
+ accessibilityElementsHidden
114
+ >
115
+ )
116
+ </Text>
117
+ </React.Fragment>
118
+ );
119
+ }
120
+
121
+ return (
122
+ <Text key={index} style={styles?.text}>
123
+ {part}
124
+ </Text>
125
+ );
126
+ });
127
+ };
128
+
129
+ return (
130
+ <ScrollView style={{ maxHeight, flex: 1, width: '100%', padding: 12 }}>
131
+ <View style={styles!.row}>
132
+ <Text style={styles!.bold} aria-role="header">
133
+ {SEVERITIES![ruleHelp.severity]}:
134
+ </Text>
135
+ <Text style={styles!.flex}>{ruleHelp.issue}</Text>
136
+ <Pressable
137
+ hitSlop={{ left: 18, top: 18, bottom: 18, right: 18 }}
138
+ role="button"
139
+ aria-label="Close Error information"
140
+ onPress={onClose}
141
+ >
142
+ <Text> ✖ </Text>
143
+ </Pressable>
144
+ </View>
145
+
146
+ <Text style={[styles!.bold, styles?.full]} aria-role="header">
147
+ Why this matters:
148
+ </Text>
149
+ <View style={[styles!.row, styles?.column]}>
150
+ <Text style={styles?.text}>{ruleHelp.message}</Text>
151
+ {issue.extra ? (
152
+ <View style={styles!.extraInline}>
153
+ {renderTextWithColorSwatches(String(issue.extra))}
154
+ </View>
155
+ ) : null}
156
+ </View>
157
+ <Text style={[styles!.bold, styles?.full]} aria-role="header">
158
+ How to fix:
159
+ </Text>
160
+ <View style={styles!.row}>{renderHowToFix(ruleHelp.howToFix)}</View>
161
+ </ScrollView>
162
+ );
163
+ }
164
+ : null;
165
+
166
+ const Code = __DEV__
167
+ ? ({ children }: { children: ReactNode }) => {
168
+ const code =
169
+ typeof children === 'string' ? children.replace(/`/g, '') : children;
170
+
171
+ return <Text style={styles!.code}> {code} </Text>;
172
+ }
173
+ : null;
174
+
175
+ const styles = __DEV__
176
+ ? StyleSheet.create({
177
+ row: {
178
+ flexDirection: 'row',
179
+ alignContent: 'center',
180
+ alignItems: 'center',
181
+ fontSize: 14,
182
+ width: '100%',
183
+ marginBottom: 12,
184
+ flex: 1,
185
+ },
186
+ bold: {
187
+ fontSize: 14,
188
+ fontWeight: 600,
189
+ paddingRight: 12,
190
+ },
191
+ full: {
192
+ width: '100%',
193
+ marginBottom: 4,
194
+ },
195
+ text: {
196
+ lineHeight: 18,
197
+ paddingBottom: 4,
198
+ },
199
+ column: {
200
+ flexDirection: 'column',
201
+ },
202
+ flex: {
203
+ flex: 1,
204
+ },
205
+ code: {
206
+ backgroundColor: '#333',
207
+ color: '#fff',
208
+ },
209
+ extraInline: {
210
+ flexDirection: 'row',
211
+ flexWrap: 'wrap',
212
+ alignItems: 'center',
213
+ },
214
+ swatchFrame: {
215
+ backgroundColor: '#000',
216
+ paddingHorizontal: 2,
217
+ marginHorizontal: 2,
218
+ lineHeight: 14,
219
+ },
220
+ swatchGlyph: {
221
+ fontSize: 11,
222
+ lineHeight: 14,
223
+ },
224
+ })
225
+ : null;
@@ -0,0 +1,58 @@
1
+ import logger from './utils/logger';
2
+ import { AmaRule, AmaRuleAction } from './utils/rules';
3
+
4
+ export const NON_OVERRIDABLE_RULES: string[] | undefined = __DEV__
5
+ ? [
6
+ 'NO_ACCESSIBILITY_ROLE',
7
+ 'NO_ACCESSIBILITY_LABEL',
8
+ 'NO_KEYBOARD_TRAP',
9
+ 'NO_UNDEFINED',
10
+ 'INPUT_HAS_NO_VISIBLE_LABEL',
11
+ 'FLATLIST_NO_COUNT_IN_PLURAL_MESSAGE',
12
+ 'BOTTOM_SHEET_CLOSE_ACTION',
13
+ 'INCOMPATIBLE_ACCESSIBILITY_STATE',
14
+ 'INCOMPATIBLE_ACCESSIBILITY_ROLE',
15
+ ]
16
+ : undefined;
17
+
18
+ export type HighlightMode = 'border' | 'background' | 'both';
19
+ export type LogMode = 'always' | 'inspect';
20
+
21
+ export type AmaProjectConfig = {
22
+ rules: Record<
23
+ | Partial<AmaRule>
24
+ | 'CONTRAST_CHECKER_MAX_DEPTH'
25
+ | 'IGNORE_CONTRAST_FOR_DISABLED_ELEMENTS',
26
+ AmaRuleAction
27
+ > | null;
28
+ accessibilityLabelExceptions: string[];
29
+ highlight: {
30
+ mode: HighlightMode;
31
+ borderWidth?: number;
32
+ gap?: number;
33
+ };
34
+ log: LogMode;
35
+ uppercaseMinLength: number;
36
+ longNumberMinLength: number;
37
+ checks: {
38
+ ui: boolean;
39
+ forms: boolean;
40
+ delay: number;
41
+ };
42
+ };
43
+
44
+ const defaultRules: AmaProjectConfig = require('./../../ama.config.json');
45
+ let projectRules = defaultRules;
46
+
47
+ try {
48
+ // look upwards to user's project root
49
+ const userDefinedRules: Partial<AmaProjectConfig> = require('./../../../../ama.config.json');
50
+ projectRules = Object.assign(projectRules, userDefinedRules);
51
+
52
+ logger?.log("Using project's config file");
53
+ } catch (_) {
54
+ logger?.log('Project config not found, using default one!');
55
+ }
56
+
57
+ logger?.log(JSON.stringify(projectRules, null, 2));
58
+ export default projectRules;
@@ -0,0 +1,11 @@
1
+ import { AmaRule, AmaRuleAction } from './utils/rules';
2
+ export type { AmaRule, AmaRuleAction };
3
+
4
+ export type Position = [number, number, number, number];
5
+
6
+ export type AmaError = {
7
+ rule: AmaRule;
8
+ extra?: any;
9
+ label?: string;
10
+ viewId: number;
11
+ };
@@ -0,0 +1,153 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+ import type { RefObject } from 'react';
3
+ import { DevSettings } from 'react-native';
4
+ import type { AmaNodes, AmaUiSnapshotsData } from '../ReactNativeAma.types';
5
+ import ReactNativeAmaModule from '../ReactNativeAmaModule';
6
+ import projectRules from './config';
7
+ import type { AmaError, AmaRule } from './types';
8
+ import type { UseAMADev } from './useAMADev';
9
+ import { getNodeIssueState, runNodeChecks } from './useAMADevNodeChecks.dev';
10
+ import {
11
+ getItemsWithNoStateUpdated,
12
+ syncUiInteractionIssues,
13
+ } from './useAMADevUiInteraction.dev';
14
+ import { amaClearHighlight } from './utils/amaClearHighlight';
15
+ import { logError } from './utils/logError';
16
+ import logger from './utils/logger';
17
+
18
+ const startAMA = __DEV__
19
+ ? () => {
20
+ logger?.log(
21
+ '👀 Start Monitoring 👀: ' + JSON.stringify(projectRules.checks)
22
+ );
23
+
24
+ ReactNativeAmaModule.start({
25
+ ...projectRules.checks,
26
+ borderWidth: projectRules.highlight?.borderWidth,
27
+ gap: projectRules.highlight?.gap,
28
+ });
29
+ }
30
+ : null;
31
+
32
+ export const useAMADev: UseAMADev | null = __DEV__
33
+ ? () => {
34
+ const isMonitoring = useRef(true);
35
+ const lastNodesChecked = useRef<AmaNodes>({});
36
+ const previousNodeIssues = useRef<AmaError[]>([]);
37
+ const jsFailedChecks = useRef<AmaError[]>([]);
38
+ const highlightedIssues = useRef<Record<number, number>>({});
39
+ const issuesRef = useRef<AmaError[]>([]);
40
+ const [issues, setIssues] = useState<AmaError[]>([]);
41
+
42
+ useEffect(() => {
43
+ issuesRef.current = issues;
44
+ }, [issues]);
45
+
46
+ const checkNodes = (nodesToCheck: AmaNodes) => {
47
+ lastNodesChecked.current = nodesToCheck;
48
+
49
+ const nodeIssues =
50
+ runNodeChecks?.({
51
+ nodesToCheck,
52
+ previousNodeIssues: previousNodeIssues.current,
53
+ jsFailedChecks: jsFailedChecks.current,
54
+ highlightedIssues: highlightedIssues.current,
55
+ }) ?? [];
56
+
57
+ setIssues(
58
+ (currentIssues) =>
59
+ getNodeIssueState?.(currentIssues, nodeIssues, nodesToCheck) ??
60
+ currentIssues
61
+ );
62
+ previousNodeIssues.current = nodeIssues;
63
+ };
64
+
65
+ const checkResultUiInteraction = (data?: AmaUiSnapshotsData) => {
66
+ if (!data) {
67
+ return;
68
+ }
69
+
70
+ const itemsToFlag = Array.from(
71
+ getItemsWithNoStateUpdated?.(data, lastNodesChecked.current) ?? []
72
+ );
73
+
74
+ setIssues(
75
+ (prevIssues) =>
76
+ syncUiInteractionIssues?.(prevIssues, itemsToFlag, data.rootTag) ??
77
+ prevIssues
78
+ );
79
+ };
80
+
81
+ const stopAMA = () => {
82
+ logger?.log('[React Native AMA]: 🙈 Stop Monitoring 🙈');
83
+
84
+ for (const issue of issuesRef.current) {
85
+ amaClearHighlight?.(issue);
86
+ }
87
+
88
+ highlightedIssues.current = {};
89
+ ReactNativeAmaModule.stop();
90
+ };
91
+
92
+ useEffect(() => {
93
+ startAMA?.();
94
+
95
+ const amaOnNodesListener = ReactNativeAmaModule.addListener(
96
+ 'onAmaNodes',
97
+ checkNodes
98
+ );
99
+ const amaOnUiInteraction = ReactNativeAmaModule.addListener(
100
+ 'onUIInteraction',
101
+ checkResultUiInteraction
102
+ );
103
+
104
+ return () => {
105
+ stopAMA();
106
+
107
+ amaOnNodesListener.remove();
108
+ amaOnUiInteraction.remove();
109
+ };
110
+ }, []);
111
+
112
+ const toggleReactNativeAMA = () => {
113
+ if (isMonitoring.current) {
114
+ stopAMA();
115
+ setIssues([]);
116
+ } else {
117
+ startAMA?.();
118
+ }
119
+
120
+ isMonitoring.current = !isMonitoring.current;
121
+ };
122
+
123
+ const trackError = (rule: AmaRule, ref?: RefObject<any>) => {
124
+ const viewId = ref?.current?.__nativeTag ?? -1;
125
+ const issue: AmaError = { rule, viewId };
126
+
127
+ logError?.(issue);
128
+
129
+ const alreadyTracked = jsFailedChecks.current.some(
130
+ (item) => item.viewId === viewId && item.rule === rule
131
+ );
132
+
133
+ if (!alreadyTracked) {
134
+ jsFailedChecks.current.push(issue);
135
+ setIssues((currentIssues) => [...currentIssues, issue]);
136
+ checkNodes(lastNodesChecked.current);
137
+ }
138
+ };
139
+
140
+ useEffect(() => {
141
+ DevSettings.addMenuItem(
142
+ 'Toggle React Native AMA',
143
+ toggleReactNativeAMA
144
+ );
145
+ // eslint-disable-next-line react-hooks/exhaustive-deps
146
+ }, []);
147
+
148
+ return {
149
+ issues,
150
+ trackError,
151
+ };
152
+ }
153
+ : null;
@@ -0,0 +1,260 @@
1
+ import { renderHook, act } from '@testing-library/react-native';
2
+ import projectRules from './config';
3
+
4
+ let mockStart: jest.Mock;
5
+ let mockStop: jest.Mock;
6
+ let mockHighlight: jest.Mock;
7
+ let mockClearHighlight: jest.Mock;
8
+ let mockAddListener: jest.Mock;
9
+ let onAmaNodesCallback: ((data: any) => void) | null = null;
10
+ let onUIInteractionCallback: ((data: any) => void) | null = null;
11
+
12
+ jest.mock('../ReactNativeAmaModule', () => {
13
+ mockStart = jest.fn();
14
+ mockStop = jest.fn();
15
+ mockHighlight = jest.fn().mockResolvedValue([0, 0, 100, 50]);
16
+ mockClearHighlight = jest.fn();
17
+ mockAddListener = jest.fn().mockImplementation((event: string, cb: any) => {
18
+ if (event === 'onAmaNodes') {
19
+ onAmaNodesCallback = cb;
20
+ } else if (event === 'onUIInteraction') {
21
+ onUIInteractionCallback = cb;
22
+ }
23
+ return { remove: jest.fn() };
24
+ });
25
+
26
+ return {
27
+ __esModule: true,
28
+ default: {
29
+ start: (...args: any[]) => mockStart(...args),
30
+ stop: (...args: any[]) => mockStop(...args),
31
+ highlight: (...args: any[]) => mockHighlight(...args),
32
+ clearHighlight: (...args: any[]) => mockClearHighlight(...args),
33
+ addListener: (...args: any[]) => mockAddListener(...args),
34
+ },
35
+ };
36
+ });
37
+
38
+ jest.mock('./utils/logFoundIssues', () => ({
39
+ logFoundIssues: jest.fn(),
40
+ }));
41
+
42
+ jest.mock('./utils/amaClearHighlight', () => ({
43
+ amaClearHighlight: jest.fn(),
44
+ }));
45
+
46
+ const makeNode = (overrides = {}) => ({
47
+ type: 'Text',
48
+ viewId: 1,
49
+ traits: ['header'],
50
+ ariaRole: 'header',
51
+ ariaLabel: 'Title',
52
+ ...overrides,
53
+ });
54
+
55
+ beforeEach(() => {
56
+ jest.clearAllMocks();
57
+ onAmaNodesCallback = null;
58
+ onUIInteractionCallback = null;
59
+ // @ts-ignore
60
+ global.__DEV__ = true;
61
+ });
62
+
63
+ afterEach(() => {
64
+ projectRules.rules = {} as typeof projectRules.rules;
65
+ });
66
+
67
+ describe('useAMADev', () => {
68
+ it('starts AMA monitoring on mount', () => {
69
+ const { useAMADev } = require('./useAMADev');
70
+ renderHook(() => useAMADev!());
71
+ expect(mockStart).toHaveBeenCalledTimes(1);
72
+ });
73
+
74
+ it('registers onAmaNodes and onUIInteraction listeners', () => {
75
+ const { useAMADev } = require('./useAMADev');
76
+ renderHook(() => useAMADev!());
77
+ expect(mockAddListener).toHaveBeenCalledWith('onAmaNodes', expect.any(Function));
78
+ expect(mockAddListener).toHaveBeenCalledWith('onUIInteraction', expect.any(Function));
79
+ });
80
+
81
+ it('stops AMA monitoring on unmount', () => {
82
+ const { useAMADev } = require('./useAMADev');
83
+ const { unmount } = renderHook(() => useAMADev!());
84
+ unmount();
85
+ expect(mockStop).toHaveBeenCalledTimes(1);
86
+ });
87
+
88
+ it('returns empty issues initially', () => {
89
+ const { useAMADev } = require('./useAMADev');
90
+ const { result } = renderHook(() => useAMADev!());
91
+ expect(result.current.issues).toEqual([]);
92
+ });
93
+
94
+ it('adds NO_HEADER_FOUND issue when no header node is present', async () => {
95
+ const { useAMADev } = require('./useAMADev');
96
+ const { result } = renderHook(() => useAMADev!());
97
+
98
+ await act(async () => {
99
+ onAmaNodesCallback?.({ 1: makeNode({ type: 'View', traits: [], ariaRole: 'button' }) });
100
+ });
101
+
102
+ expect(result.current.issues).toContainEqual(
103
+ expect.objectContaining({ rule: 'NO_HEADER_FOUND' }),
104
+ );
105
+ });
106
+
107
+ it('does not add NO_HEADER_FOUND when a header node is present', async () => {
108
+ const { useAMADev } = require('./useAMADev');
109
+ const { result } = renderHook(() => useAMADev!());
110
+
111
+ await act(async () => {
112
+ onAmaNodesCallback?.({ 1: makeNode({ type: 'Text', ariaRole: 'header' }) });
113
+ });
114
+
115
+ const headerIssues = result.current.issues.filter(
116
+ (i: any) => i.rule === 'NO_HEADER_FOUND',
117
+ );
118
+ expect(headerIssues).toHaveLength(0);
119
+ });
120
+
121
+ it('highlights a component when a Pressable has no accessibility label', async () => {
122
+ const { useAMADev } = require('./useAMADev');
123
+ renderHook(() => useAMADev!());
124
+
125
+ await act(async () => {
126
+ onAmaNodesCallback?.({
127
+ 1: makeNode({ type: 'Text', ariaRole: 'header', viewId: 1 }),
128
+ 42: {
129
+ type: 'Pressable',
130
+ viewId: 42,
131
+ traits: ['button'],
132
+ ariaRole: 'button',
133
+ ariaLabel: undefined,
134
+ width: 50,
135
+ height: 50,
136
+ },
137
+ });
138
+ });
139
+
140
+ expect(mockHighlight).toHaveBeenCalledWith(42, expect.any(String), expect.any(String), expect.any(Number));
141
+ });
142
+
143
+ it('handles checkResultUiInteraction with no data gracefully', async () => {
144
+ const { useAMADev } = require('./useAMADev');
145
+ renderHook(() => useAMADev!());
146
+
147
+ await act(async () => {
148
+ onUIInteractionCallback?.(undefined);
149
+ });
150
+
151
+ expect(mockHighlight).not.toHaveBeenCalled();
152
+ });
153
+
154
+ it('clears highlights for fixed issues when new check has fewer problems', async () => {
155
+ const { amaClearHighlight } = require('./utils/amaClearHighlight');
156
+ const { useAMADev } = require('./useAMADev');
157
+ renderHook(() => useAMADev!());
158
+
159
+ const badPressable = {
160
+ type: 'Pressable',
161
+ viewId: 99,
162
+ traits: ['button'],
163
+ ariaRole: 'button',
164
+ ariaLabel: undefined,
165
+ width: 50,
166
+ height: 50,
167
+ };
168
+
169
+ await act(async () => {
170
+ onAmaNodesCallback?.({
171
+ 1: makeNode({ type: 'Text', ariaRole: 'header', viewId: 1 }),
172
+ 99: badPressable,
173
+ });
174
+ });
175
+
176
+ expect(mockHighlight).toHaveBeenCalled();
177
+
178
+ await act(async () => {
179
+ onAmaNodesCallback?.({
180
+ 1: makeNode({ type: 'Text', ariaRole: 'header', viewId: 1 }),
181
+ 99: { ...badPressable, ariaLabel: 'Fixed label' },
182
+ });
183
+ });
184
+
185
+ expect(amaClearHighlight).toHaveBeenCalled();
186
+ });
187
+
188
+ it('checkResultUiInteraction flags NO_ACCESSIBILITY_STATE_SET when state did not change after tap', async () => {
189
+ const { useAMADev } = require('./useAMADev');
190
+ const { result } = renderHook(() => useAMADev!());
191
+
192
+ await act(async () => {
193
+ onAmaNodesCallback?.({
194
+ 1: makeNode({ type: 'Text', ariaRole: 'header', viewId: 1 }),
195
+ 100: makeNode({ type: 'Pressable', viewId: 100, ariaRole: 'button', ariaLabel: 'Toggle', traits: ['button'] }),
196
+ });
197
+ });
198
+
199
+ const snapshot = { label: 'Toggle', role: 'button', isChecked: false, isBusy: false, isSelected: false, isDisabled: false, isExpanded: false, parentId: 0 };
200
+
201
+ await act(async () => {
202
+ onUIInteractionCallback?.({
203
+ rootTag: 100,
204
+ beforeModalVisible: false,
205
+ afterModalVisible: false,
206
+ before: { 100: { ...snapshot, label: 'Toggle changed' } },
207
+ after: { 100: snapshot },
208
+ afterSettled: { 100: snapshot },
209
+ });
210
+ });
211
+
212
+ expect(result.current.issues.some((i: any) => i.rule === 'NO_ACCESSIBILITY_STATE_SET')).toBe(true);
213
+ });
214
+
215
+ it('checkResultUiInteraction clears NO_ACCESSIBILITY_STATE_SET when state did change', async () => {
216
+ const { useAMADev } = require('./useAMADev');
217
+ const { result } = renderHook(() => useAMADev!());
218
+
219
+ await act(async () => {
220
+ onAmaNodesCallback?.({
221
+ 1: makeNode({ type: 'Text', ariaRole: 'header', viewId: 1 }),
222
+ 100: makeNode({ type: 'Pressable', viewId: 100, ariaRole: 'button', ariaLabel: 'Toggle', traits: ['button'] }),
223
+ });
224
+ });
225
+
226
+ const snapshot = { label: 'Toggle', role: 'button', isChecked: false, isBusy: false, isSelected: false, isDisabled: false, isExpanded: false, parentId: 0 };
227
+
228
+ await act(async () => {
229
+ onUIInteractionCallback?.({
230
+ rootTag: 100,
231
+ beforeModalVisible: false,
232
+ afterModalVisible: false,
233
+ before: { 100: { ...snapshot, isChecked: false, label: 'changed' } },
234
+ after: { 100: { ...snapshot, isChecked: true } },
235
+ afterSettled: { 100: { ...snapshot, isChecked: true } },
236
+ });
237
+ });
238
+
239
+ expect(result.current.issues.some((i: any) => i.rule === 'NO_ACCESSIBILITY_STATE_SET')).toBe(false);
240
+ });
241
+
242
+ it('checkResultUiInteraction does nothing when rootTag is not in afterKeys', async () => {
243
+ const { useAMADev } = require('./useAMADev');
244
+ const { result } = renderHook(() => useAMADev!());
245
+
246
+ await act(async () => {
247
+ onUIInteractionCallback?.({
248
+ rootTag: 999,
249
+ beforeModalVisible: false,
250
+ afterModalVisible: false,
251
+ before: {},
252
+ after: { 1: { label: 'btn' } },
253
+ afterSettled: null,
254
+ });
255
+ });
256
+
257
+ expect(result.current.issues.some((i: any) => i.rule === 'NO_ACCESSIBILITY_STATE_SET')).toBe(false);
258
+ });
259
+
260
+ });