* Copyright (c) 2025 Huawei Technologies Co., Ltd.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {TestSuite} from '@rnoh/testerino';
import {useEffect, useRef, useState} from 'react';
import {
AccessibilityChangeEventName,
AccessibilityEventTypes,
AccessibilityInfo,
findNodeHandle,
Text,
TextProps,
View,
} from 'react-native';
import {Button, Ref, TestCase} from '../components';
export function AccessibilityInfoTest() {
const [skipMsgIfReaderDisabled, setSkipMsgIfReaderDisabled] = useState<
undefined | string
>('Determining screen reader status...');
useEffect(() => {
const onScreenReaderChange = (isScreenReaderEnabled: boolean) => {
if (isScreenReaderEnabled) {
setSkipMsgIfReaderDisabled(undefined);
} else {
setSkipMsgIfReaderDisabled(
'ScreenReader must be enabled to run this test',
);
}
};
AccessibilityInfo.isScreenReaderEnabled().then(onScreenReaderChange);
const listener = AccessibilityInfo.addEventListener(
'screenReaderChanged',
onScreenReaderChange,
);
return () => {
listener.remove();
};
}, []);
return (
<TestSuite name="AccessibilityInfo">
<TestSuite name="addEventListener">
<TestSuite name="accessibilityServiceChanged">
<TestCase.Example itShould="display red background if Accessibility Service is changed">
<AccessibilityInfoAccessibilityServiceChanged />
</TestCase.Example>
</TestSuite>
<TestSuite name="screenReaderChanged">
<TestCase.Manual
initialState={false}
itShould="pass when screen reader is enabled (enable screen reader)"
arrange={({setState}) => {
return (
<AccessibilityInfoScreenReaderStatus
onScreenReaderChanged={isEnabled => {
if (isEnabled) {
setState(true);
}
}}
/>
);
}}
assert={({expect, state}) => {
expect(state).to.be.eq(true);
}}
/>
</TestSuite>
</TestSuite>
<TestSuite name="isScreenReaderEnabled">
<AccessibilityFeatureStatusTestCase
itShould="return true when the screen reader is enabled and false when disabled (test passes when call history includes both states)"
onChangeAccessibilityFeature={() =>
AccessibilityInfo.isScreenReaderEnabled()
}
/>
</TestSuite>
<TestSuite name="announceForAccessibility">
<TestCase.Example
skip={skipMsgIfReaderDisabled}
itShould="announce 'Button pressed' after pressing the button">
<Button
label="Read text"
onPress={() => {
AccessibilityInfo.announceForAccessibility('Button pressed');
}}
/>
</TestCase.Example>
</TestSuite>
<TestSuite name="isAccessibilityServiceEnabled">
<AccessibilityFeatureStatusTestCase
itShould="return true when the screen reader (or some other third party accessibility service) is enabled and false when disabled (test passes when call history includes both states)"
onChangeAccessibilityFeature={() =>
AccessibilityInfo.isAccessibilityServiceEnabled()
}
/>
</TestSuite>
<TestSuite name="sendAccessibilityEvent">
<TestSuite name="click">
<TestCase.Example
itShould="announce 'checked' after pressing the button"
skip={
skipMsgIfReaderDisabled ?? {
android: false,
harmony:
'Connected but this test fails probably because aria-checked is not connected.',
}
}>
<SendAccessibilityEventExample
accessibilityEvent="click"
textProps={{'aria-checked': true}}
/>
</TestCase.Example>
<TestCase.Example
itShould="announce 'selected' after pressing the button"
skip={
skipMsgIfReaderDisabled ?? {
android: false,
harmony:
'Connected but this test fails probably because aria-selected is not connected.',
}
}>
<SendAccessibilityEventExample
accessibilityEvent="click"
textProps={{'aria-selected': true}}
/>
</TestCase.Example>
</TestSuite>
<TestSuite name="focus">
<TestCase.Example
itShould="read the text content in the square"
skip={skipMsgIfReaderDisabled}>
<SendAccessibilityEventExample accessibilityEvent="focus" />
</TestCase.Example>
</TestSuite>
<TestSuite name="viewHoverEnter">
<TestCase.Example
itShould="read the text content in the square when hovering over an element"
skip={
skipMsgIfReaderDisabled ?? {
android: false,
harmony:
"Hovering in general behaves differently to Android and the text isn't read.",
}
}>
<SendAccessibilityEventExample accessibilityEvent="viewHoverEnter" />
</TestCase.Example>
</TestSuite>
</TestSuite>
<TestSuite name="setAccessibilityFocus">
<TestCase.Example
itShould="read 'target view' after pressing the button and switch focus to that view"
skip={skipMsgIfReaderDisabled}>
<Ref<Text>
render={ref => {
return (
<>
<Text ref={ref}>target view</Text>
<Button
label="setAccessibilityFocus on target view"
onPress={() => {
AccessibilityInfo.setAccessibilityFocus(
findNodeHandle(ref.current)!,
);
}}
/>
</>
);
}}
/>
</TestCase.Example>
</TestSuite>
<TestSuite name="isBoldTextEnabled">
<TestCase.Manual<boolean[]>
itShould="pass when history contains both states"
initialState={[]}
skip={{android: true, harmony: false}}
arrange={({setState, state}) => {
return (
<>
<Text style={{fontSize: 12}}>
{
"To change font weight on HarmonyOS, go to 'Settings' > 'Text & display size' and drag the Text weight slider until the OS classifies the text weight as bold"
}
</Text>
<Text style={{marginTop: 16}}>
Call result history: {JSON.stringify(state)}
</Text>
<Button
label="check text weight"
onPress={() => {
AccessibilityInfo.isBoldTextEnabled().then(
isBoldTextEnabled => {
setState(prev => [...prev, isBoldTextEnabled]);
},
);
}}
/>
</>
);
}}
assert={async ({expect, state}) => {
await new Promise(resolve => {
if (state.includes(true) && state.includes(false)) {
resolve(undefined);
}
});
expect(state.includes(true)).to.be.true;
expect(state.includes(false)).to.be.true;
}}
/>
</TestSuite>
</TestSuite>
);
}
function SendAccessibilityEventExample({
accessibilityEvent,
textProps,
}: {
accessibilityEvent: AccessibilityEventTypes;
textProps?: TextProps;
}) {
const ref = useRef<Text>(null);
return (
<>
<Text
ref={ref}
accessible={true}
style={{
backgroundColor: 'silver',
width: '100%',
height: 64,
}}
{...(textProps ?? {})}>
target content
</Text>
<Button
label="SEND ACCESSIBILITY EVENT"
onPress={() => {
AccessibilityInfo.sendAccessibilityEvent(
ref.current!,
accessibilityEvent,
);
}}
/>
</>
);
}
function AccessibilityInfoScreenReaderStatus({
onScreenReaderChanged,
}: {
onScreenReaderChanged: (isEnabled: boolean) => void;
}) {
const [isScreenReaderEnabled, setIsScreenReaderEnabled] = useState(false);
const backgroundColor = isScreenReaderEnabled ? 'green' : 'transparent';
useEffect(() => {
const listener = AccessibilityInfo.addEventListener(
'screenReaderChanged',
isEnabled => {
setIsScreenReaderEnabled(isEnabled);
onScreenReaderChanged(isEnabled);
},
);
AccessibilityInfo.isScreenReaderEnabled().then(isEnabled => {
setIsScreenReaderEnabled(isEnabled);
onScreenReaderChanged(isEnabled);
});
return () => {
listener.remove();
};
}, []);
return (
<View style={{backgroundColor, padding: 16}}>
<Text>{`Current Screen Reader status: ${isScreenReaderEnabled ? 'enabled' : 'disabled'}`}</Text>
</View>
);
}
function AccessibilityInfoAccessibilityServiceChanged() {
const [isEnabled, setIsEnabled] = useState(false);
const backgroundColor = isEnabled ? 'red' : 'transparent';
useEffect(() => {
const listener = AccessibilityInfo.addEventListener(
'accessibilityServiceChanged' as AccessibilityChangeEventName,
(isOn: boolean) => {
setIsEnabled(isOn);
},
);
return function cleanup() {
listener.remove();
};
}, []);
return (
<View style={{backgroundColor, padding: 16}}>
<Text>{`Accessibility Service change is ${isEnabled ? 'enabled' : 'disabled'}.`}</Text>
</View>
);
}
function AccessibilityFeatureStatusTestCase({
itShould,
onChangeAccessibilityFeature,
skip,
}: {
itShould: string;
onChangeAccessibilityFeature: () => Promise<boolean>;
skip?: React.ComponentProps<typeof TestCase.Manual>['skip'];
}) {
return (
<TestCase.Manual<boolean[]>
itShould={itShould}
initialState={[]}
skip={skip}
arrange={({setState, state}) => {
return (
<>
<Text>Call history: {JSON.stringify(state)}</Text>
<Button
label="trigger function"
onPress={() => {
onChangeAccessibilityFeature().then(isScreenReaderEnabled => {
setState(prev => [...prev, isScreenReaderEnabled]);
});
}}
/>
</>
);
}}
assert={async ({expect, state}) => {
await new Promise(resolve => {
if (state.includes(true) && state.includes(false)) {
resolve(undefined);
}
});
expect(state.includes(true)).to.be.true;
expect(state.includes(false)).to.be.true;
}}
/>
);
}