Skip to content

Commit

Permalink
Fix crash from non-adjustable unit RangeCell a11y activation (#30636)
Browse files Browse the repository at this point in the history
* Avoid invoking possible null callback

Previously, the unit picker callback was always invoked when a11y tools
activated the element. This caused the app to crash whenever the element
did not have adjustable units.

* Add test coverage for RangeCell accessibility actions
  • Loading branch information
dcalhoun authored Apr 8, 2021
1 parent 616dc9a commit 0652cff
Show file tree
Hide file tree
Showing 3 changed files with 102 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,9 @@ class BottomSheetRangeCell extends Component {
this.a11yDecrementValue();
break;
case 'activate':
openUnitPicker();
if ( openUnitPicker ) {
openUnitPicker();
}
break;
}
} }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/**
* External dependencies
*/
import { AccessibilityInfo } from 'react-native';
import { create, act } from 'react-test-renderer';

/**
* Internal dependencies
*/
import RangeCell from '../range-cell';

// Avoid errors due to mocked stylesheet files missing required selectors
jest.mock( '@wordpress/compose', () => ( {
...jest.requireActual( '@wordpress/compose' ),
withPreferredColorScheme: jest.fn( ( Component ) => ( props ) => (
<Component
{ ...props }
preferredColorScheme={ {} }
getStylesFromColorScheme={ jest.fn( () => ( {} ) ) }
/>
) ),
} ) );

const isScreenReaderEnabled = Promise.resolve( true );
beforeAll( () => {
// Mock async native module to avoid act warning
AccessibilityInfo.isScreenReaderEnabled = jest.fn(
() => isScreenReaderEnabled
);
} );

it( 'allows modifying units via a11y actions', async () => {
const mockOpenUnitPicker = jest.fn();
const renderer = create(
<RangeCell
label="Opacity"
minimumValue={ 0 }
maximumValue={ 100 }
value={ 50 }
onChange={ jest.fn() }
openUnitPicker={ mockOpenUnitPicker }
/>
);
// Await async update to component state to avoid act warning
await act( () => isScreenReaderEnabled );
const { onAccessibilityAction } = renderer.toJSON().props;

onAccessibilityAction( { nativeEvent: { actionName: 'activate' } } );
expect( mockOpenUnitPicker ).toHaveBeenCalled();
} );

describe( 'when range lacks an adjustable unit', () => {
it( 'disallows modifying units via a11y actions', async () => {
const renderer = create(
<RangeCell
label="Opacity"
minimumValue={ 0 }
maximumValue={ 100 }
value={ 50 }
onChange={ jest.fn() }
/>
);
// Await async update to component state to avoid act warning
await act( () => isScreenReaderEnabled );
const { onAccessibilityAction } = renderer.toJSON().props;

expect( () =>
onAccessibilityAction( { nativeEvent: { actionName: 'activate' } } )
).not.toThrow();
} );
} );
31 changes: 28 additions & 3 deletions test/native/setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,15 @@
*/
import 'react-native-gesture-handler/jestSetup';

// Mock component to render with props rather than merely a string name so that
// we may assert against it. ...args is used avoid warnings about ignoring
// forwarded refs if React.forwardRef happens to be used.
const mockComponent = ( element ) => ( ...args ) => {
const [ props ] = args;
const React = require( 'react' );
return React.createElement( element, props, props.children );
};

jest.mock( '@wordpress/element', () => {
return {
__esModule: true,
Expand Down Expand Up @@ -81,9 +90,14 @@ jest.mock( 'react-native-safe-area', () => {
};
} );

jest.mock( '@react-native-community/slider', () => () => 'Slider', {
virtual: true,
} );
jest.mock(
'@react-native-community/slider',
() => {
const { forwardRef } = require( 'react' );
return forwardRef( mockComponent( 'Slider' ) );
},
{ virtual: true }
);

if ( ! global.window.matchMedia ) {
global.window.matchMedia = () => ( {
Expand Down Expand Up @@ -126,3 +140,14 @@ jest.mock( 'react-native/Libraries/Animated/src/NativeAnimatedHelper' );
// undefined." The private module referenced could possibly be replaced with
// a React ref instead. We could then remove this internal mock.
jest.mock( 'react-native/Libraries/Components/TextInput/TextInputState' );

// Mock native modules incompatible with testing environment
jest.mock(
'react-native/Libraries/Components/AccessibilityInfo/AccessibilityInfo',
() => ( {
addEventListener: jest.fn(),
announceForAccessibility: jest.fn(),
removeEventListener: jest.fn(),
isScreenReaderEnabled: jest.fn( () => Promise.resolve( false ) ),
} )
);

0 comments on commit 0652cff

Please sign in to comment.