@react-native-ama/core 0.0.1 → 0.0.2
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.
- package/package.json +2 -2
- package/src/components/AMAProvider.integration.test.tsx +145 -0
- package/src/components/AMAProvider.test.tsx +199 -0
- package/src/components/AMAProvider.tsx +231 -0
- package/src/components/AutofocusContainer.test.tsx +69 -0
- package/src/components/AutofocusContainer.tsx +43 -0
- package/src/components/HideChildrenFromAccessibilityTree.test.tsx +128 -0
- package/src/components/HideChildrenFromAccessibilityTree.tsx +55 -0
- package/src/hooks/useButtonChecks.ts +73 -0
- package/src/hooks/useChecks.test.ts +214 -0
- package/src/hooks/useChecks.ts +207 -0
- package/src/hooks/useFocus.test.ts +98 -0
- package/src/hooks/useFocus.ts +42 -0
- package/src/hooks/useTimedAction.android.test.ts +39 -0
- package/src/hooks/useTimedAction.ios.test.ts +46 -0
- package/src/hooks/useTimedAction.ts +40 -0
- package/src/index.ts +16 -0
- package/src/utils/numerify.test.ts +11 -0
- package/src/utils/numerify.ts +3 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { act, render } from '@testing-library/react-native';
|
|
2
|
+
import { flushMicroTasks } from '@testing-library/react-native/build/flush-micro-tasks';
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { Pressable, Text } from 'react-native';
|
|
5
|
+
|
|
6
|
+
import { AMAProvider } from './AMAProvider';
|
|
7
|
+
import { HideChildrenFromAccessibilityTree } from './HideChildrenFromAccessibilityTree';
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
jest.clearAllMocks();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
describe('HideChildrenFromAccessibilityTree', () => {
|
|
14
|
+
describe('iOS', () => {
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
const Platform = jest.requireActual(
|
|
17
|
+
'react-native/Libraries/Utilities/Platform',
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
Platform.select = (params: any) => {
|
|
21
|
+
return params.ios;
|
|
22
|
+
};
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('hides all the children from the accessibility tree when the screen reader is enabled', async () => {
|
|
26
|
+
const { getByTestId } = await renderHideChildrenFromAccessibilityTree();
|
|
27
|
+
|
|
28
|
+
['test-text', 'test-button', 'nested-text'].forEach(testID => {
|
|
29
|
+
expect(getByTestId(testID).props).toEqual(
|
|
30
|
+
expect.objectContaining({
|
|
31
|
+
importantForAccessibility: 'no',
|
|
32
|
+
accessibilityElementsHidden: true,
|
|
33
|
+
}),
|
|
34
|
+
);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('does nothing if the SR is turned off %s', async () => {
|
|
39
|
+
const { getByTestId } = await renderHideChildrenFromAccessibilityTree({
|
|
40
|
+
isScreenReaderEnabled: false,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
['test-text', 'test-button', 'nested-text'].forEach(testID => {
|
|
44
|
+
expect(getByTestId(testID).props).not.toHaveProperty(
|
|
45
|
+
'importantForAccessibility',
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
expect(getByTestId(testID).props).not.toHaveProperty(
|
|
49
|
+
'accessibilityElementsHidden',
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe('Android', () => {
|
|
56
|
+
beforeEach(() => {
|
|
57
|
+
const Platform = jest.requireActual(
|
|
58
|
+
'react-native/Libraries/Utilities/Platform',
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
Platform.select = (params: any) => {
|
|
62
|
+
return params.default;
|
|
63
|
+
};
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('hides all the children from the accessibility tree when the screen reader is enabled', async () => {
|
|
67
|
+
const { getByTestId } = await renderHideChildrenFromAccessibilityTree();
|
|
68
|
+
|
|
69
|
+
expect(getByTestId('test-wrapper').props).toEqual(
|
|
70
|
+
expect.objectContaining({
|
|
71
|
+
importantForAccessibility: 'no-hide-descendants',
|
|
72
|
+
}),
|
|
73
|
+
);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('does nothing if the SR is turned off %s', async () => {
|
|
77
|
+
const { queryByTestId } = await renderHideChildrenFromAccessibilityTree({
|
|
78
|
+
isScreenReaderEnabled: false,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
expect(queryByTestId('test-wrapper')).toBeNull();
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const renderHideChildrenFromAccessibilityTree = async (params?: {
|
|
87
|
+
isScreenReaderEnabled: boolean;
|
|
88
|
+
}) => {
|
|
89
|
+
useAMAContext.mockReturnValue({
|
|
90
|
+
isScreenReaderEnabled: params?.isScreenReaderEnabled ?? true,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const renderAPI = render(
|
|
94
|
+
<AMAProvider>
|
|
95
|
+
<HideChildrenFromAccessibilityTree testID="test-wrapper">
|
|
96
|
+
<Text testID="test-text">Test here</Text>
|
|
97
|
+
<>
|
|
98
|
+
<Pressable testID="test-button">
|
|
99
|
+
<Text testID="nested-text">Press me</Text>
|
|
100
|
+
</Pressable>
|
|
101
|
+
</>
|
|
102
|
+
</HideChildrenFromAccessibilityTree>
|
|
103
|
+
</AMAProvider>,
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
await act(async () => {
|
|
107
|
+
await flushMicroTasks();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
return renderAPI;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
let useAMAContext: jest.Mock;
|
|
114
|
+
|
|
115
|
+
function mockAMAProvider() {
|
|
116
|
+
const originalModule = jest.requireActual('./AMAProvider');
|
|
117
|
+
|
|
118
|
+
useAMAContext = jest.fn().mockReturnValue({
|
|
119
|
+
isScreenReaderEnabled: true,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
...originalModule,
|
|
124
|
+
useAMAContext,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
jest.mock('./AMAProvider', () => mockAMAProvider());
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import React, { PropsWithChildren } from 'react';
|
|
2
|
+
import { Platform, View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { useAMAContext } from './AMAProvider';
|
|
5
|
+
|
|
6
|
+
type HideChildrenFromAccessibilityTreeProps = {
|
|
7
|
+
testID?: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const HideChildrenFromAccessibilityTree = (
|
|
11
|
+
props: PropsWithChildren<HideChildrenFromAccessibilityTreeProps>,
|
|
12
|
+
): JSX.Element => {
|
|
13
|
+
const { isScreenReaderEnabled } = useAMAContext();
|
|
14
|
+
|
|
15
|
+
if (!isScreenReaderEnabled) {
|
|
16
|
+
return <>{props.children}</>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return Platform.select({
|
|
20
|
+
default: <HideChildrenFromAccessibilityAndroid {...props} />,
|
|
21
|
+
ios: <HideChildrenFromAccessibilityTreeIOS {...props} />,
|
|
22
|
+
});
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const HideChildrenFromAccessibilityAndroid = ({
|
|
26
|
+
children,
|
|
27
|
+
testID,
|
|
28
|
+
}: PropsWithChildren<HideChildrenFromAccessibilityTreeProps>) => {
|
|
29
|
+
return (
|
|
30
|
+
<View importantForAccessibility="no-hide-descendants" testID={testID}>
|
|
31
|
+
{children}
|
|
32
|
+
</View>
|
|
33
|
+
);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const HideChildrenFromAccessibilityTreeIOS = ({
|
|
37
|
+
children,
|
|
38
|
+
}: PropsWithChildren<{}>) => {
|
|
39
|
+
return hideChildrenFromAccessibilityTree(children);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const hideChildrenFromAccessibilityTree = (component: React.ReactNode): any => {
|
|
43
|
+
return (
|
|
44
|
+
React.Children.map(component, child => {
|
|
45
|
+
return React.isValidElement(child)
|
|
46
|
+
? React.cloneElement(child, {
|
|
47
|
+
// @ts-ignore
|
|
48
|
+
importantForAccessibility: 'no',
|
|
49
|
+
accessibilityElementsHidden: true,
|
|
50
|
+
children: hideChildrenFromAccessibilityTree(child.props.children),
|
|
51
|
+
})
|
|
52
|
+
: child;
|
|
53
|
+
}) || null
|
|
54
|
+
);
|
|
55
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { applyStyle } from '@react-native-ama/internal';
|
|
2
|
+
import type React from 'react';
|
|
3
|
+
import type { PressableStateCallbackType } from 'react-native';
|
|
4
|
+
|
|
5
|
+
import { useChecks } from './useChecks';
|
|
6
|
+
|
|
7
|
+
export const useButtonChecks = __DEV__
|
|
8
|
+
? (
|
|
9
|
+
props: Record<string, any>,
|
|
10
|
+
children?:
|
|
11
|
+
| React.ReactNode
|
|
12
|
+
| ((state: PressableStateCallbackType) => React.ReactNode)
|
|
13
|
+
| undefined,
|
|
14
|
+
shouldPerformContrastCheck: boolean = true,
|
|
15
|
+
) => {
|
|
16
|
+
const checks = useChecks();
|
|
17
|
+
if (checks === null) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
const {
|
|
21
|
+
noUndefinedProperty,
|
|
22
|
+
contrastChecker,
|
|
23
|
+
onLayout,
|
|
24
|
+
noUppercaseStringChecker,
|
|
25
|
+
checkCompatibleAccessibilityState,
|
|
26
|
+
checkAccessibilityRole,
|
|
27
|
+
debugStyle,
|
|
28
|
+
} = checks;
|
|
29
|
+
|
|
30
|
+
let style = props.style || {};
|
|
31
|
+
|
|
32
|
+
const isAccessible = props.accessible !== false;
|
|
33
|
+
|
|
34
|
+
isAccessible &&
|
|
35
|
+
noUndefinedProperty({
|
|
36
|
+
properties: props,
|
|
37
|
+
property: 'accessibilityRole',
|
|
38
|
+
rule: 'NO_ACCESSIBILITY_ROLE',
|
|
39
|
+
});
|
|
40
|
+
isAccessible &&
|
|
41
|
+
noUndefinedProperty({
|
|
42
|
+
properties: props,
|
|
43
|
+
property: 'accessibilityLabel',
|
|
44
|
+
rule: 'NO_ACCESSIBILITY_LABEL',
|
|
45
|
+
});
|
|
46
|
+
isAccessible &&
|
|
47
|
+
noUppercaseStringChecker({
|
|
48
|
+
text: props.accessibilityLabel,
|
|
49
|
+
});
|
|
50
|
+
checkCompatibleAccessibilityState({
|
|
51
|
+
accessibilityStates: props?.accessibilityState,
|
|
52
|
+
accessibilityRole: props?.accessiblityRole,
|
|
53
|
+
});
|
|
54
|
+
checkAccessibilityRole(props.accessibilityRole);
|
|
55
|
+
|
|
56
|
+
const contrastCheckerCallback = shouldPerformContrastCheck
|
|
57
|
+
? contrastChecker
|
|
58
|
+
: undefined;
|
|
59
|
+
|
|
60
|
+
const theStyle = applyStyle?.({
|
|
61
|
+
style,
|
|
62
|
+
debugStyle,
|
|
63
|
+
// @ts-ignore
|
|
64
|
+
children,
|
|
65
|
+
contrastCheckerCallback,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
onLayout,
|
|
70
|
+
debugStyle: theStyle,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
: null;
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import * as Logger from '@react-native-ama/internal';
|
|
2
|
+
import { type RuleAction } from '@react-native-ama/internal';
|
|
3
|
+
import * as CheckForAccessibilityRole from '@react-native-ama/internal/src/checks/checkAccessibilityRole';
|
|
4
|
+
import * as CheckForAccessibilityState from '@react-native-ama/internal/src/checks/checkForAccessibilityState';
|
|
5
|
+
import { renderHook } from '@testing-library/react-native';
|
|
6
|
+
|
|
7
|
+
import * as AMAProvider from '../components/AMAProvider';
|
|
8
|
+
import { useChecks } from './useChecks';
|
|
9
|
+
|
|
10
|
+
const trackError = jest.fn();
|
|
11
|
+
const removeError = jest.fn();
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
jest.clearAllMocks();
|
|
15
|
+
|
|
16
|
+
jest.spyOn(console, 'warn').mockImplementation();
|
|
17
|
+
jest.spyOn(console, 'error').mockImplementation();
|
|
18
|
+
jest.spyOn(console, 'info').mockImplementation();
|
|
19
|
+
|
|
20
|
+
jest.spyOn(AMAProvider, 'useAMAContext').mockReturnValue({
|
|
21
|
+
trackError,
|
|
22
|
+
removeError,
|
|
23
|
+
} as any);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe('useChecks', () => {
|
|
27
|
+
it.each(['MUST_NOT', 'MUST'])(
|
|
28
|
+
'tracks the error only if the rule is %s',
|
|
29
|
+
ruleValue => {
|
|
30
|
+
jest
|
|
31
|
+
// @ts-ignore
|
|
32
|
+
.spyOn(Logger, 'getRuleAction')
|
|
33
|
+
// @ts-ignore
|
|
34
|
+
.mockReturnValue(ruleValue as RuleAction);
|
|
35
|
+
|
|
36
|
+
const { result } = renderHook(() => useChecks());
|
|
37
|
+
|
|
38
|
+
result.current?.logResult('test-me', {
|
|
39
|
+
rule: 'NO_UNDEFINED',
|
|
40
|
+
extra: 'extra',
|
|
41
|
+
message: 'message',
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
expect(trackError).toHaveBeenCalledTimes(1);
|
|
45
|
+
expect(trackError).toHaveBeenCalledWith(expect.any(String));
|
|
46
|
+
},
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
it.each(['SHOULD', 'SHOULD_NOT'])(
|
|
50
|
+
'does not track the error if the rule is %s',
|
|
51
|
+
ruleValue => {
|
|
52
|
+
const spy = jest.spyOn(console, 'warn');
|
|
53
|
+
|
|
54
|
+
jest
|
|
55
|
+
// @ts-ignore
|
|
56
|
+
.spyOn(Logger, 'getRuleAction')
|
|
57
|
+
// @ts-ignore
|
|
58
|
+
.mockReturnValue(ruleValue as RuleAction);
|
|
59
|
+
|
|
60
|
+
const { result } = renderHook(() => useChecks());
|
|
61
|
+
|
|
62
|
+
result.current?.logResult('test-me', {
|
|
63
|
+
rule: 'NO_UNDEFINED',
|
|
64
|
+
extra: 'extra',
|
|
65
|
+
message: 'message',
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
expect(trackError).not.toHaveBeenCalled();
|
|
69
|
+
|
|
70
|
+
expect(spy).toHaveBeenCalled();
|
|
71
|
+
},
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
it.each(['MUST_NOT', 'MUST'])(
|
|
75
|
+
'tracks the same error only once',
|
|
76
|
+
ruleValue => {
|
|
77
|
+
jest
|
|
78
|
+
// @ts-ignore
|
|
79
|
+
.spyOn(Logger, 'getRuleAction')
|
|
80
|
+
// @ts-ignore
|
|
81
|
+
.mockReturnValue(ruleValue as RuleAction);
|
|
82
|
+
|
|
83
|
+
const { result } = renderHook(() => useChecks());
|
|
84
|
+
|
|
85
|
+
result.current?.logResult('test-me', {
|
|
86
|
+
rule: 'NO_UNDEFINED',
|
|
87
|
+
extra: 'extra',
|
|
88
|
+
message: 'message',
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
expect(trackError).toHaveBeenCalledTimes(1);
|
|
92
|
+
|
|
93
|
+
trackError.mockClear();
|
|
94
|
+
|
|
95
|
+
result.current?.logResult('test-me', {
|
|
96
|
+
rule: 'NO_UNDEFINED',
|
|
97
|
+
extra: 'extra',
|
|
98
|
+
message: 'message',
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
expect(trackError).toHaveBeenCalledTimes(0);
|
|
102
|
+
},
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
it('removes the tracked error after has been fixed', () => {
|
|
106
|
+
// @ts-ignore
|
|
107
|
+
jest.spyOn(Logger, 'getRuleAction').mockReturnValue('MUST');
|
|
108
|
+
|
|
109
|
+
const { result } = renderHook(() => useChecks());
|
|
110
|
+
|
|
111
|
+
result.current?.logResult('test-me', {
|
|
112
|
+
rule: 'NO_UNDEFINED',
|
|
113
|
+
extra: 'extra',
|
|
114
|
+
message: 'message',
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
expect(trackError).toHaveBeenCalledWith(expect.any(String));
|
|
118
|
+
|
|
119
|
+
result.current?.logResult('test-me', null);
|
|
120
|
+
|
|
121
|
+
expect(removeError).toHaveBeenCalledWith(trackError.mock.calls[0][0]);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('checks for compatible accessibility state', () => {
|
|
125
|
+
const check = jest
|
|
126
|
+
.spyOn(CheckForAccessibilityState, 'checkForAccessibilityState')
|
|
127
|
+
.mockReturnValue(null);
|
|
128
|
+
const { result } = renderHook(() => useChecks());
|
|
129
|
+
|
|
130
|
+
result.current?.checkCompatibleAccessibilityState({
|
|
131
|
+
hello: 'world',
|
|
132
|
+
busy: true,
|
|
133
|
+
checked: 'wow',
|
|
134
|
+
accessibilityRole: 'button',
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
expect(check).toHaveBeenCalledWith({
|
|
138
|
+
accessibilityRole: 'button',
|
|
139
|
+
accessibilityState: undefined,
|
|
140
|
+
checked: 'wow',
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('checks for compatible accessibility state when accessibilityState is passed', () => {
|
|
145
|
+
const check = jest
|
|
146
|
+
.spyOn(CheckForAccessibilityState, 'checkForAccessibilityState')
|
|
147
|
+
.mockReturnValue(null);
|
|
148
|
+
const { result } = renderHook(() => useChecks());
|
|
149
|
+
|
|
150
|
+
result.current?.checkCompatibleAccessibilityState({
|
|
151
|
+
hello: 'world',
|
|
152
|
+
checked: false,
|
|
153
|
+
accessibilityState: {
|
|
154
|
+
selected: 'yeah',
|
|
155
|
+
},
|
|
156
|
+
accessibilityRole: 'button',
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
expect(check).toHaveBeenCalledWith({
|
|
160
|
+
accessibilityRole: 'button',
|
|
161
|
+
accessibilityState: {
|
|
162
|
+
selected: 'yeah',
|
|
163
|
+
},
|
|
164
|
+
checked: false,
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('checks for compatible accessibility role', () => {
|
|
169
|
+
const check = jest
|
|
170
|
+
.spyOn(CheckForAccessibilityRole, 'checkAccessibilityRole')
|
|
171
|
+
.mockReturnValue(null);
|
|
172
|
+
const { result } = renderHook(() => useChecks());
|
|
173
|
+
|
|
174
|
+
result.current?.checkAccessibilityRole('button');
|
|
175
|
+
|
|
176
|
+
expect(check).toHaveBeenCalledWith('button');
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
jest.mock('react-native', () => {
|
|
181
|
+
const ReactNative = jest.requireActual('react-native');
|
|
182
|
+
|
|
183
|
+
// Extend ReactNative
|
|
184
|
+
return Object.setPrototypeOf(
|
|
185
|
+
{
|
|
186
|
+
// Redefine an export, like a component
|
|
187
|
+
Button: 'Button',
|
|
188
|
+
// Mock out properties of an already mocked export
|
|
189
|
+
LayoutAnimation: {
|
|
190
|
+
...ReactNative.LayoutAnimation,
|
|
191
|
+
configureNext: jest.fn(),
|
|
192
|
+
},
|
|
193
|
+
Platform: {
|
|
194
|
+
...ReactNative.Platform,
|
|
195
|
+
OS: 'ios',
|
|
196
|
+
Version: 123,
|
|
197
|
+
isTesting: true,
|
|
198
|
+
select: (objs: { ios: any }) => objs.ios,
|
|
199
|
+
},
|
|
200
|
+
// Mock a native module
|
|
201
|
+
NativeModules: {
|
|
202
|
+
...ReactNative.NativeModules,
|
|
203
|
+
Override: { great: 'success' },
|
|
204
|
+
},
|
|
205
|
+
InteractionManager: {
|
|
206
|
+
// @ts-ignore
|
|
207
|
+
runAfterInteractions: callback => {
|
|
208
|
+
callback();
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
ReactNative,
|
|
213
|
+
);
|
|
214
|
+
});
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ERROR_STYLE,
|
|
3
|
+
LogParams,
|
|
4
|
+
checkAccessibilityRole as checkAccessibilityRoleImplementation,
|
|
5
|
+
checkFocusTrap as checkFocusTrapImplementation,
|
|
6
|
+
checkForAccessibilityState,
|
|
7
|
+
checkMinimumSize as checkMinimumSizeImplementation,
|
|
8
|
+
contrastChecker as contrastCheckerImplementation,
|
|
9
|
+
getRuleAction,
|
|
10
|
+
logFailure,
|
|
11
|
+
noUndefinedProperty as noUndefinedPropertyImplementation,
|
|
12
|
+
uppercaseStringChecker as noUppercaseStringCheckerImplementation,
|
|
13
|
+
uppercaseChecker as uppercaseCheckerImplementation,
|
|
14
|
+
type CheckFocusTrap,
|
|
15
|
+
type CheckForAccessibilityState,
|
|
16
|
+
type ContrastChecker,
|
|
17
|
+
type NoUndefinedProperty,
|
|
18
|
+
type UppercaseChecker,
|
|
19
|
+
type UppercaseStringChecker,
|
|
20
|
+
} from '@react-native-ama/internal';
|
|
21
|
+
import * as React from 'react';
|
|
22
|
+
import {
|
|
23
|
+
AccessibilityRole,
|
|
24
|
+
InteractionManager,
|
|
25
|
+
LayoutChangeEvent,
|
|
26
|
+
} from 'react-native';
|
|
27
|
+
|
|
28
|
+
import { isDevContextValue, useAMAContext } from '../components/AMAProvider';
|
|
29
|
+
|
|
30
|
+
export const useChecks = () => {
|
|
31
|
+
const context = useAMAContext();
|
|
32
|
+
|
|
33
|
+
if (!isDevContextValue(context)) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Because the AMA Error view cannot be removed by default, this hacky solution
|
|
39
|
+
* allows the dev to hide it if all the issues have been solved.
|
|
40
|
+
*/
|
|
41
|
+
const fakeRandom = React.useRef('' + Date.now() + Math.random() * 42);
|
|
42
|
+
const hasErrors = React.useRef(false);
|
|
43
|
+
const failedTests = React.useRef<string[]>([]);
|
|
44
|
+
const shouldCheckLayout = React.useRef(true);
|
|
45
|
+
const layoutCheckTimeout = React.useRef<NodeJS.Timeout>();
|
|
46
|
+
const [minimumSizeFailed, setMinimumSizeFailed] = React.useState(false);
|
|
47
|
+
const [debugStyle, setDebugStyle] = React.useState<any>({});
|
|
48
|
+
|
|
49
|
+
const { trackError, removeError } = context;
|
|
50
|
+
|
|
51
|
+
const logResult = (
|
|
52
|
+
name: string,
|
|
53
|
+
result: LogParams | LogParams[] | null,
|
|
54
|
+
): Record<string, any> => {
|
|
55
|
+
const index = failedTests.current.indexOf(name);
|
|
56
|
+
|
|
57
|
+
if (result === null) {
|
|
58
|
+
if (index >= 0 && hasErrors.current) {
|
|
59
|
+
failedTests.current.splice(index);
|
|
60
|
+
|
|
61
|
+
hasErrors.current = false;
|
|
62
|
+
|
|
63
|
+
InteractionManager.runAfterInteractions(() => {
|
|
64
|
+
removeError(fakeRandom.current);
|
|
65
|
+
|
|
66
|
+
setTimeout(() => {
|
|
67
|
+
setDebugStyle({});
|
|
68
|
+
}, 100);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const results = Array.isArray(result) ? result : [result];
|
|
74
|
+
|
|
75
|
+
results.forEach(logParam => {
|
|
76
|
+
if (logParam === null) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const action = getRuleAction?.(logParam.rule);
|
|
81
|
+
const hasFailed = action === 'MUST_NOT' || action === 'MUST';
|
|
82
|
+
|
|
83
|
+
if (index < 0) {
|
|
84
|
+
// @ts-ignore
|
|
85
|
+
logFailure?.({ action, ...logParam });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (!hasFailed) {
|
|
89
|
+
return;
|
|
90
|
+
} else if (index >= 0) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
failedTests.current.push(name);
|
|
95
|
+
|
|
96
|
+
InteractionManager.runAfterInteractions(() => {
|
|
97
|
+
trackError(fakeRandom.current);
|
|
98
|
+
|
|
99
|
+
setDebugStyle(ERROR_STYLE);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
hasErrors.current = true;
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// @ts-ignore
|
|
106
|
+
return hasErrors.current ? ERROR_STYLE : {};
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const noUndefinedProperty = <T>(params: NoUndefinedProperty<T>) =>
|
|
110
|
+
logResult(
|
|
111
|
+
`noUndefinedProperty ${params.property as string}`,
|
|
112
|
+
noUndefinedPropertyImplementation(params),
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
const contrastChecker = (params: ContrastChecker) =>
|
|
116
|
+
logResult('contrastChecker', contrastCheckerImplementation(params));
|
|
117
|
+
|
|
118
|
+
const checkMinimumSize = (params: LayoutChangeEvent) => {
|
|
119
|
+
return logResult(
|
|
120
|
+
'checkMinimumSize',
|
|
121
|
+
checkMinimumSizeImplementation(params),
|
|
122
|
+
);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const noUppercaseStringChecker = (params: UppercaseStringChecker) =>
|
|
126
|
+
logResult(
|
|
127
|
+
'accessibilityLabelChecker',
|
|
128
|
+
noUppercaseStringCheckerImplementation(params),
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
const uppercaseChecker = (params: UppercaseChecker) =>
|
|
132
|
+
logResult('uppercaseChecker', uppercaseCheckerImplementation(params));
|
|
133
|
+
|
|
134
|
+
const checkFocusTrap = (params: CheckFocusTrap) => {
|
|
135
|
+
checkFocusTrapImplementation(params).then(result => {
|
|
136
|
+
logResult('checkFocusTrap', result);
|
|
137
|
+
});
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const checkAccessibilityRole = (param: AccessibilityRole) => {
|
|
141
|
+
logResult(
|
|
142
|
+
'checkAccessibilityRole',
|
|
143
|
+
checkAccessibilityRoleImplementation(param),
|
|
144
|
+
);
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const checkCompatibleAccessibilityState = (props: Record<string, any>) => {
|
|
148
|
+
const amaStates = ['checked', 'selected'];
|
|
149
|
+
|
|
150
|
+
const params: CheckForAccessibilityState = {
|
|
151
|
+
accessibilityRole: props.accessibilityRole,
|
|
152
|
+
accessibilityState: props?.accessibilityState,
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
amaStates.forEach(amaState => {
|
|
156
|
+
if (amaState in props) {
|
|
157
|
+
// @ts-ignore
|
|
158
|
+
params[amaState] = props[amaState];
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
logResult(
|
|
163
|
+
'checkCompatibleAccessibilityState',
|
|
164
|
+
checkForAccessibilityState(params),
|
|
165
|
+
);
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const onLayout = (event: LayoutChangeEvent) => {
|
|
169
|
+
/**
|
|
170
|
+
* When the check fails there are situation when adding a border makes the
|
|
171
|
+
* component meet the minimum size requirement, and this causes a loop as the
|
|
172
|
+
* state is update continuously between true and false.
|
|
173
|
+
* To "avoid" that we wait at least 100ms before checking the size again, as
|
|
174
|
+
* this gives the dev time to hot-reload the changes.
|
|
175
|
+
*/
|
|
176
|
+
if (!shouldCheckLayout.current) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
shouldCheckLayout.current = false;
|
|
181
|
+
|
|
182
|
+
// @ts-ignore
|
|
183
|
+
clearTimeout(layoutCheckTimeout.current);
|
|
184
|
+
|
|
185
|
+
layoutCheckTimeout.current = setTimeout(() => {
|
|
186
|
+
shouldCheckLayout.current = true;
|
|
187
|
+
}, 1000);
|
|
188
|
+
|
|
189
|
+
const result = checkMinimumSize(event);
|
|
190
|
+
|
|
191
|
+
setMinimumSizeFailed(Object.keys(result).length > 0);
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
logResult,
|
|
196
|
+
noUndefinedProperty,
|
|
197
|
+
contrastChecker,
|
|
198
|
+
onLayout,
|
|
199
|
+
noUppercaseStringChecker,
|
|
200
|
+
uppercaseChecker,
|
|
201
|
+
checkFocusTrap,
|
|
202
|
+
minimumSizeFailed,
|
|
203
|
+
checkCompatibleAccessibilityState,
|
|
204
|
+
checkAccessibilityRole,
|
|
205
|
+
debugStyle,
|
|
206
|
+
};
|
|
207
|
+
};
|