Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/jest-preset/jest/mocks/AccessibilityInfo.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ const AccessibilityInfo = {
$FlowFixMe,
$FlowFixMe,
>,
getContrastLevel: jest.fn(() => Promise.resolve(0)) as JestMockFn<
$FlowFixMe,
$FlowFixMe,
>,
isBoldTextEnabled: jest.fn(() => Promise.resolve(false)) as JestMockFn<
$FlowFixMe,
$FlowFixMe,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import NativeAccessibilityManagerIOS from './NativeAccessibilityManager';
// Events that are only supported on Android.
type AccessibilityEventDefinitionsAndroid = {
accessibilityServiceChanged: [boolean],
contrastLevelChanged: [number],
highTextContrastChanged: [boolean],
};

Expand Down Expand Up @@ -53,6 +54,7 @@ const EventNames: Map<keyof AccessibilityEventDefinitions, string> =
['change', 'touchExplorationDidChange'],
['reduceMotionChanged', 'reduceMotionDidChange'],
['highTextContrastChanged', 'highTextContrastDidChange'],
['contrastLevelChanged', 'contrastLevelDidChange'],
['screenReaderChanged', 'touchExplorationDidChange'],
['accessibilityServiceChanged', 'accessibilityServiceDidChange'],
['invertColorsChanged', 'invertColorDidChange'],
Expand Down Expand Up @@ -225,6 +227,45 @@ const AccessibilityInfo = {
}
},

/**
* Query the contrast level chosen in the system settings.
*
* Returns a promise which resolves to a number from -1 to 1, where 0 is the standard contrast and
* higher values mean more contrast. This is the closest Android counterpart to "Increase
* Contrast" on iOS. Requires Android 14 (API 34) or later.
*
* The promise also resolves to 0 on Android versions before 14, and when the device has no
* `UiModeManager`, so callers can't tell these cases apart from the standard contrast. It rejects
* if the native method is missing, for example when the native code is older than the JavaScript.
*
* On iOS this always resolves to 0; use `isDarkerSystemColorsEnabled` to check whether "Increase
* Contrast" is on.
*
* @platform android
*/
getContrastLevel(): Promise<number> {
if (Platform.OS === 'android') {
return new Promise((resolve, reject) => {
if (NativeAccessibilityInfoAndroid?.getContrastLevel != null) {
NativeAccessibilityInfoAndroid.getContrastLevel(resolve);
} else {
// This is a new API, so there's no old value to keep resolving: a missing native
// method rejects, as `isShowBordersEnabled` does, rather than looking like the
// standard contrast.
reject(
new Error(
'NativeAccessibilityInfoAndroid.getContrastLevel is not available',
),
);
}
});
} else {
// iOS has no contrast level, only the on/off "Increase Contrast" setting, which
// `isDarkerSystemColorsEnabled` reports.
return Promise.resolve(0);
}
},

/**
* Query whether darker system colors is currently enabled.
*
Expand Down Expand Up @@ -445,6 +486,9 @@ const AccessibilityInfo = {
*
* These events are only supported on Android:
*
* - `contrastLevelChanged`: Android-only event. Fires when the contrast level changes. The
* argument to the event handler is a number from -1 to 1 (see `getContrastLevel`). Requires
* Android 14 (API 34) or later.
* - `highTextContrastChanged`: Android-only event. Fires when the state of the high text contrast
* toggle changes. The argument to the event handler is a boolean. The boolean is `true` when
* high text contrast is enabled and `false` otherwise.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/

import typeof RCTDeviceEventEmitterType from '../../../EventEmitter/RCTDeviceEventEmitter';
import typeof AccessibilityInfoType from '../AccessibilityInfo';

const invariant = require('invariant');

jest.unmock('../AccessibilityInfo');

type LoadedModules = {
AccessibilityInfo: AccessibilityInfoType,
RCTDeviceEventEmitter: RCTDeviceEventEmitterType,
};

// `AccessibilityInfo` picks its native event names when it is first loaded,
// so each platform needs a fresh module registry.
function loadForPlatform(os: 'android' | 'ios'): LoadedModules {
let modules: ?LoadedModules = null;
jest.isolateModules(() => {
const Platform = require('../../../Utilities/Platform').default;
/* $FlowFixMe[incompatible-type] */
Platform.OS = os;
modules = {
AccessibilityInfo: require('../AccessibilityInfo').default,
RCTDeviceEventEmitter:
require('../../../EventEmitter/RCTDeviceEventEmitter').default,
};
});
invariant(modules != null, 'Expected modules to load');
return modules;
}

describe('AccessibilityInfo contrastLevelChanged', () => {
it('delivers contrastLevelDidChange as contrastLevelChanged on Android', () => {
const {AccessibilityInfo, RCTDeviceEventEmitter} =
loadForPlatform('android');
const handler = jest.fn();

const subscription = AccessibilityInfo.addEventListener(
'contrastLevelChanged',
handler,
);
RCTDeviceEventEmitter.emit('contrastLevelDidChange', 0.5);
RCTDeviceEventEmitter.emit('contrastLevelDidChange', -0.25);
subscription.remove();
RCTDeviceEventEmitter.emit('contrastLevelDidChange', 1);

expect(handler).toHaveBeenCalledTimes(2);
expect(handler).toHaveBeenNthCalledWith(1, 0.5);
expect(handler).toHaveBeenNthCalledWith(2, -0.25);
});

it('does not subscribe on iOS', () => {
const {AccessibilityInfo, RCTDeviceEventEmitter} = loadForPlatform('ios');
const handler = jest.fn();

const subscription = AccessibilityInfo.addEventListener(
'contrastLevelChanged',
handler,
);
RCTDeviceEventEmitter.emit('contrastLevelDidChange', 0.5);
subscription.remove();

expect(handler).not.toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ let mockNativeAccessibilityManagerIOS: {

const ANDROID_RECOMMENDED_TIMEOUT = 6000;
const mockIsHighTextContrastEnabled = jest.fn(onSuccess => onSuccess(true));
const mockGetContrastLevel = jest.fn(onSuccess => onSuccess(0.5));
const mockIsGrayscaleEnabled = jest.fn(onSuccess => onSuccess(true));
const mockIsInvertColorsEnabled = jest.fn(onSuccess => onSuccess(true));
const mockIsReduceMotionEnabled = jest.fn(onSuccess => onSuccess(true));
Expand All @@ -125,6 +126,10 @@ const mockGetRecommendedTimeoutMillis = jest.fn((originalTimeout, onSuccess) =>
onSuccess(ANDROID_RECOMMENDED_TIMEOUT),
);
let mockNativeAccessibilityInfoAndroid: {
getContrastLevel: JestMockFn<
[onSuccess: (contrastLevel: number) => void],
void,
> | null,
isHighTextContrastEnabled: JestMockFn<
[onSuccess: (isHighTextContrastEnabled: boolean) => void],
void,
Expand Down Expand Up @@ -154,6 +159,7 @@ let mockNativeAccessibilityInfoAndroid: {
void,
> | null,
} = {
getContrastLevel: mockGetContrastLevel,
isHighTextContrastEnabled: mockIsHighTextContrastEnabled,
isGrayscaleEnabled: mockIsGrayscaleEnabled,
isInvertColorsEnabled: mockIsInvertColorsEnabled,
Expand Down Expand Up @@ -760,6 +766,70 @@ describe('AccessibilityInfo', () => {
});
});

describe('getContrastLevel', () => {
describe('Android', () => {
it('should call getContrastLevel if available', async () => {
/* $FlowFixMe[incompatible-type] */
Platform.OS = 'android';

const contrastLevel = await AccessibilityInfo.getContrastLevel();

expect(mockGetContrastLevel).toHaveBeenCalled();
expect(contrastLevel).toBe(0.5);
});

it('should throw error if getContrastLevel is not available', async () => {
/* $FlowFixMe[incompatible-type] */
Platform.OS = 'android';

mockNativeAccessibilityInfoAndroid.getContrastLevel = null;

const result: unknown =
await AccessibilityInfo.getContrastLevel().catch(e => e);

invariant(
result instanceof Error,
'Expected getContrastLevel to reject',
);
expect(result.message).toEqual(
'NativeAccessibilityInfoAndroid.getContrastLevel is not available',
);
});

it('should throw error if NativeAccessibilityInfoAndroid is not available', async () => {
/* $FlowFixMe[incompatible-type] */
Platform.OS = 'android';

const nativeAccessibilityInfoModule = jest.requireMock(
'../NativeAccessibilityInfo',
);
nativeAccessibilityInfoModule.default = null;

const result: unknown =
await AccessibilityInfo.getContrastLevel().catch(e => e);

invariant(
result instanceof Error,
'Expected getContrastLevel to reject',
);
expect(result.message).toEqual(
'NativeAccessibilityInfoAndroid.getContrastLevel is not available',
);
});
});

describe('iOS', () => {
it('should return 0', async () => {
/* $FlowFixMe[incompatible-type] */
Platform.OS = 'ios';

const contrastLevel = await AccessibilityInfo.getContrastLevel();

expect(contrastLevel).toBe(0);
});
});
});

describe('isHighTextContrastEnabled', () => {
describe('Android', () => {
it('should call isHighTextContrastEnabled if available', async () => {
Expand Down Expand Up @@ -860,6 +930,7 @@ describe('AccessibilityInfo', () => {
mockNativeAccessibilityManagerIOS;

mockNativeAccessibilityInfoAndroid = {
getContrastLevel: mockGetContrastLevel,
isHighTextContrastEnabled: mockIsHighTextContrastEnabled,
isGrayscaleEnabled: mockIsGrayscaleEnabled,
isInvertColorsEnabled: mockIsInvertColorsEnabled,
Expand Down
Loading
Loading