Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { cleanup, render } from '@testing-library/react-native';
import React from 'react';
import type { ViewProps } from 'react-native';
import { findNodeHandle, View } from 'react-native';

import {
Expand All @@ -12,7 +13,7 @@
import { VirtualDetector } from '../v3/detectors/VirtualDetector/VirtualDetector';

jest.mock('react-native-worklets', () =>
require('react-native-worklets/src/mock')

Check warning on line 16 in packages/react-native-gesture-handler/src/__tests__/Errors.test.tsx

View workflow job for this annotation

GitHub Actions / check

Unsafe return of an `any` typed value
);

// Reanimated 4.6.0 throws on import under Jest (`setCSSEventHandler` is not
Expand All @@ -31,7 +32,7 @@
useComposedEventHandler: jest.fn(() => jest.fn()),
isSharedValue: (value: unknown): boolean =>
value !== null && typeof value === 'object' && 'value' in value,
useSharedValue: <T,>(value: T) => ({ value }),

Check warning on line 35 in packages/react-native-gesture-handler/src/__tests__/Errors.test.tsx

View workflow job for this annotation

GitHub Actions / check

If your function doesn't call any Hooks, avoid the 'use' prefix. Instead, write it as a regular function without the 'use' prefix

Check warning on line 35 in packages/react-native-gesture-handler/src/__tests__/Errors.test.tsx

View workflow job for this annotation

GitHub Actions / check

If your function doesn't call any Hooks, avoid the 'use' prefix. Instead, write it as a regular function without the 'use' prefix
setGestureState: jest.fn(),
},
Worklets: undefined,
Expand Down Expand Up @@ -59,6 +60,41 @@
'VirtualGestureDetector must be a descendant of an InterceptingGestureDetector'
);
});

test('virtual detector reports a child that swallows the ref', () => {
const consoleError = jest
.spyOn(console, 'error')
.mockImplementation(() => undefined);

function SwallowsRef({ style }: { style?: ViewProps['style'] }) {
return <View style={style} />;
}

function VirtualDetectorSwallowedRef() {
const tap = useTapGesture({});
return (
<GestureHandlerRootView>
<InterceptingGestureDetector>
<VirtualDetector gesture={tap}>
<SwallowsRef />
</VirtualDetector>
</InterceptingGestureDetector>
</GestureHandlerRootView>
);
}

const screen = render(<VirtualDetectorSwallowedRef />);
screen.rerender(<VirtualDetectorSwallowedRef />);

expect(consoleError).toHaveBeenCalledTimes(1);
expect(consoleError).toHaveBeenCalledWith(
expect.stringContaining(
'VirtualGestureDetector could not attach to its child (SwallowsRef)'
)
);
consoleError.mockRestore();
});

test('virtual detector does not handle animated events', () => {
(findNodeHandle as jest.Mock).mockReturnValue(123);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import type { ComponentType, ReactElement } from 'react';
import {
Children,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { findNodeHandle, Platform } from 'react-native';

import { tagMessage } from '../../../utils';
Expand All @@ -14,6 +22,17 @@ import {
} from './useInterceptingDetectorContext';
import { Wrap } from './Wrap';

function getChildName(children: unknown) {
const { type } = Children.only(children) as ReactElement<
unknown,
string | ComponentType<unknown>
>;

return typeof type === 'string'
? type
: (type.displayName ?? type.name ?? 'Unknown');
}

function useRequiredInterceptingDetectorContext() {
const context = useInterceptingDetectorContext();
if (!context) {
Expand Down Expand Up @@ -58,6 +77,24 @@ export function VirtualDetector<

useNativeGestureRole(viewRef, props.children);

// Dev-only: no host instance after a commit means the child did not hand
// its `ref` to one. A child that renders null first is reported as well.
const reportedRef = useRef(false);

useEffect(() => {
if (!__DEV__ || reportedRef.current || viewRef.current) {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
return;
}

reportedRef.current = true;

console.error(
tagMessage(
`VirtualGestureDetector could not attach to its child (${getChildName(props.children)}). The child must be a host view or forward its \`ref\` to one.`
)
);
});

const handlerTags = useMemo(
() =>
isComposedGesture(props.gesture)
Expand Down
Loading