Skip to content
Merged
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