Skip to content

Commit

Permalink
[RNMobile] Add integration tests to cover Drag & Drop functionality (#…
Browse files Browse the repository at this point in the history
…41364)

* Add testID prop to Draggable components

* Add unit tests for Draggable component

* Set draggingId shared value before enable dragging

This change is required for testing, otherwise the dragging id is not passed when the dragging gesture begins.

* Mock generateHapticFeedback function

* Add testID to draggable chip component

* Add testID to BlockDraggable component

* Add test helpers for BlockDraggable component

Additionally, helpers related to fake timers have been added and updated in the global helpers file.

* Add drag and drop integration tests

* Update react-native-aztec mock to use AztecInputState
  • Loading branch information
fluiddot authored May 31, 2022
1 parent 3e9a9a8 commit 29a170b
Show file tree
Hide file tree
Showing 12 changed files with 1,038 additions and 43 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export default function BlockDraggableChip( { icon } ) {
);

return (
<View style={ [ containerStyle, shadowStyle ] }>
<View style={ [ containerStyle, shadowStyle ] } testID="draggable-chip">
<BlockIcon icon={ dragHandle } />
{ icon && <BlockIcon icon={ icon } /> }
</View>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,7 @@ const BlockDraggableWrapper = ( { children, isRTL } ) => {
onDragStart={ startDragging }
onDragOver={ updateDragging }
onDragEnd={ stopDragging }
testID="block-draggable-wrapper"
>
{ children( { onScroll: scrollHandler } ) }
</Draggable>
Expand Down Expand Up @@ -302,6 +303,7 @@ const BlockDraggableWrapper = ( { children, isRTL } ) => {
* @param {string} props.clientId Client id of the block.
* @param {string} [props.draggingClientId] Client id to use for dragging. If not defined, the value from `clientId` will be used.
* @param {boolean} [props.enabled] Enables the draggable trigger.
* @param {string} [props.testID] Id used for querying the long-press gesture handler in tests.
*
* @return {Function} Render function which includes the parameter `isDraggable` to determine if the block can be dragged.
*/
Expand All @@ -310,6 +312,7 @@ const BlockDraggable = ( {
children,
draggingClientId,
enabled = true,
testID,
} ) => {
const wasBeingDragged = useRef( false );
const [ isEditingText, setIsEditingText ] = useState( false );
Expand Down Expand Up @@ -446,6 +449,7 @@ const BlockDraggable = ( {
android: DEFAULT_LONG_PRESS_MIN_DURATION,
} ) }
onLongPress={ onLongPressDraggable }
testID={ testID }
>
<Animated.View style={ wrapperStyles }>
{ children( { isDraggable: true } ) }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`BlockDraggable moves blocks: Initial order 1`] = `
"<!-- wp:paragraph -->
<p>This is a paragraph.</p>
<!-- /wp:paragraph -->
<!-- wp:image {\\"sizeSlug\\":\\"large\\"} -->
<figure class=\\"wp-block-image size-large\\"><img src=\\"https://cldup.com/cXyG__fTLN.jpg\\" alt=\\"\\"/></figure>
<!-- /wp:image -->
<!-- wp:spacer -->
<div style=\\"height:100px\\" aria-hidden=\\"true\\" class=\\"wp-block-spacer\\"></div>
<!-- /wp:spacer -->
<!-- wp:gallery {\\"linkTo\\":\\"none\\"} -->
<figure class=\\"wp-block-gallery has-nested-images columns-default is-cropped\\"><!-- wp:image {\\"sizeSlug\\":\\"large\\",\\"linkDestination\\":\\"none\\"} -->
<figure class=\\"wp-block-image size-large\\"><img src=\\"https://cldup.com/cXyG__fTLN.jpg\\" alt=\\"\\"/></figure>
<!-- /wp:image -->
<!-- wp:image {\\"sizeSlug\\":\\"large\\",\\"linkDestination\\":\\"none\\"} -->
<figure class=\\"wp-block-image size-large\\"><img src=\\"https://cldup.com/cXyG__fTLN.jpg\\" alt=\\"\\"/></figure>
<!-- /wp:image --></figure>
<!-- /wp:gallery -->"
`;
exports[`BlockDraggable moves blocks: Paragraph block moved from first to second position 1`] = `
"<!-- wp:image {\\"sizeSlug\\":\\"large\\"} -->
<figure class=\\"wp-block-image size-large\\"><img src=\\"https://cldup.com/cXyG__fTLN.jpg\\" alt=\\"\\"/></figure>
<!-- /wp:image -->
<!-- wp:paragraph -->
<p>This is a paragraph.</p>
<!-- /wp:paragraph -->
<!-- wp:spacer -->
<div style=\\"height:100px\\" aria-hidden=\\"true\\" class=\\"wp-block-spacer\\"></div>
<!-- /wp:spacer -->
<!-- wp:gallery {\\"linkTo\\":\\"none\\"} -->
<figure class=\\"wp-block-gallery has-nested-images columns-default is-cropped\\"><!-- wp:image {\\"sizeSlug\\":\\"large\\",\\"linkDestination\\":\\"none\\"} -->
<figure class=\\"wp-block-image size-large\\"><img src=\\"https://cldup.com/cXyG__fTLN.jpg\\" alt=\\"\\"/></figure>
<!-- /wp:image -->
<!-- wp:image {\\"sizeSlug\\":\\"large\\",\\"linkDestination\\":\\"none\\"} -->
<figure class=\\"wp-block-image size-large\\"><img src=\\"https://cldup.com/cXyG__fTLN.jpg\\" alt=\\"\\"/></figure>
<!-- /wp:image --></figure>
<!-- /wp:gallery -->"
`;
exports[`BlockDraggable moves blocks: Spacer block moved from third to first position 1`] = `
"<!-- wp:spacer -->
<div style=\\"height:100px\\" aria-hidden=\\"true\\" class=\\"wp-block-spacer\\"></div>
<!-- /wp:spacer -->
<!-- wp:image {\\"sizeSlug\\":\\"large\\"} -->
<figure class=\\"wp-block-image size-large\\"><img src=\\"https://cldup.com/cXyG__fTLN.jpg\\" alt=\\"\\"/></figure>
<!-- /wp:image -->
<!-- wp:paragraph -->
<p>This is a paragraph.</p>
<!-- /wp:paragraph -->
<!-- wp:gallery {\\"linkTo\\":\\"none\\"} -->
<figure class=\\"wp-block-gallery has-nested-images columns-default is-cropped\\"><!-- wp:image {\\"sizeSlug\\":\\"large\\",\\"linkDestination\\":\\"none\\"} -->
<figure class=\\"wp-block-image size-large\\"><img src=\\"https://cldup.com/cXyG__fTLN.jpg\\" alt=\\"\\"/></figure>
<!-- /wp:image -->
<!-- wp:image {\\"sizeSlug\\":\\"large\\",\\"linkDestination\\":\\"none\\"} -->
<figure class=\\"wp-block-image size-large\\"><img src=\\"https://cldup.com/cXyG__fTLN.jpg\\" alt=\\"\\"/></figure>
<!-- /wp:image --></figure>
<!-- /wp:gallery -->"
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
/**
* External dependencies
*/
import {
act,
fireEvent,
initializeEditor,
waitForStoreResolvers,
within,
advanceAnimationByFrame,
} from 'test/helpers';
import { fireGestureHandler } from 'react-native-gesture-handler/jest-utils';
import { State } from 'react-native-gesture-handler';

// Touch event type constants have been extracted from original source code, as they are not exported in the package.
// Reference: https://github.com/software-mansion/react-native-gesture-handler/blob/90895e5f38616a6be256fceec6c6a391cd9ad7c7/src/TouchEventType.ts
export const TouchEventType = {
UNDETERMINED: 0,
TOUCHES_DOWN: 1,
TOUCHES_MOVE: 2,
TOUCHES_UP: 3,
TOUCHES_CANCELLED: 4,
};

const DEFAULT_TOUCH_EVENTS = [
{
id: 1,
eventType: TouchEventType.TOUCHES_DOWN,
x: 0,
y: 0,
},
];

/**
* @typedef {Object} WPBlockWithLayout
* @property {string} name Name of the block (e.g. Paragraph).
* @property {string} html HTML content.
* @property {Object} layout Layout data.
* @property {Object} layout.x X position.
* @property {Object} layout.y Y position.
* @property {Object} layout.width Width.
* @property {Object} layout.height Height.
*/

/**
* Initialize the editor with an array of blocks that include their HTML and layout.
*
* @param {WPBlockWithLayout[]} blocks Initial blocks.
*
* @return {import('@testing-library/react-native').RenderAPI} The Testing Library screen.
*/
export const initializeWithBlocksLayouts = async ( blocks ) => {
const initialHtml = blocks.map( ( block ) => block.html ).join( '\n' );

const screen = await initializeEditor( { initialHtml } );
const { getByA11yLabel } = screen;

const waitPromises = [];
blocks.forEach( ( block, index ) => {
const a11yLabel = new RegExp(
`${ block.name } Block\\. Row ${ index + 1 }`
);
const element = getByA11yLabel( a11yLabel );
// "onLayout" event will populate the blocks layouts data.
fireEvent( element, 'layout', {
nativeEvent: { layout: block.layout },
} );
if ( block.nestedBlocks ) {
// Nested blocks are rendered via the FlatList of the inner block list.
// In order to render the items of a FlatList, it's required to trigger the
// "onLayout" event. Additionally, the call is wrapped over "waitForStoreResolvers"
// because the nested blocks might make API requests (e.g. the Gallery block).
waitPromises.push(
waitForStoreResolvers( () =>
fireEvent(
within( element ).getByTestId( 'block-list-wrapper' ),
'layout',
{
nativeEvent: {
layout: {
width: block.layout.width,
height: block.layout.height,
},
},
}
)
)
);

block.nestedBlocks.forEach( ( nestedBlock, nestedIndex ) => {
const nestedA11yLabel = new RegExp(
`${ nestedBlock.name } Block\\. Row ${ nestedIndex + 1 }`
);
fireEvent(
within( element ).getByA11yLabel( nestedA11yLabel ),
'layout',
{
nativeEvent: { layout: nestedBlock.layout },
}
);
} );
}
} );
await Promise.all( waitPromises );

return screen;
};

/**
* Fires long-press gesture event on a block.
*
* @param {import('react-test-renderer').ReactTestInstance} block Block test instance.
* @param {string} testID Id for querying the draggable trigger element.
* @param {Object} [options] Configuration options for the gesture event.
* @param {boolean} [options.failed] Determines if the gesture should fail.
* @param {number} [options.triggerIndex] In case there are multiple draggable triggers, this specifies the index to use.
*/
export const fireLongPress = (
block,
testID,
{ failed = false, triggerIndex } = {}
) => {
const draggableTrigger =
typeof triggerIndex !== 'undefined'
? within( block ).getAllByTestId( testID )[ triggerIndex ]
: within( block ).getByTestId( testID );
if ( failed ) {
fireGestureHandler( draggableTrigger, [ { state: State.FAILED } ] );
} else {
fireGestureHandler( draggableTrigger, [
{ oldState: State.BEGAN, state: State.ACTIVE },
{ state: State.ACTIVE },
{ state: State.END },
] );
}
// Advance timers one frame to ensure that shared values
// are updated and trigger animation reactions.
act( () => advanceAnimationByFrame( 1 ) );
};

/**
* Fires pan gesture event on a BlockDraggable component.
*
* @param {import('react-test-renderer').ReactTestInstance} blockDraggable BlockDraggable test instance.
* @param {Object} [touchEvents] Array of touch events to dispatch on the pan gesture.
*/
export const firePanGesture = (
blockDraggable,
touchEvents = DEFAULT_TOUCH_EVENTS
) => {
const gestureTouchEvents = touchEvents.map(
( { eventType, ...touchEvent } ) => ( {
allTouches: [ touchEvent ],
eventType,
} )
);
fireGestureHandler( blockDraggable, [
// TOUCHES_DOWN event is only received on ACTIVE state, so we have to fire it manually.
{ oldState: State.BEGAN, state: State.ACTIVE },
...gestureTouchEvents,
{ state: State.END },
] );
// Advance timers one frame to ensure that shared values
// are updated and trigger animation reactions.
act( () => advanceAnimationByFrame( 1 ) );
};

/**
* Gets the draggable chip element.
*
* @param {import('@testing-library/react-native').RenderAPI} screen The Testing Library screen.
*
* @return {import('react-test-renderer').ReactTestInstance} Draggable chip test instance.
*/
export const getDraggableChip = ( { getByTestId } ) => {
let draggableChip;
try {
draggableChip = getByTestId( 'draggable-chip' );
} catch ( e ) {
// NOOP.
}
return draggableChip;
};
Loading

0 comments on commit 29a170b

Please sign in to comment.