callstack / react-native-testing-library

🦉 Simple and complete React Native testing utilities that encourage good testing practices.
https://callstack.github.io/react-native-testing-library/
MIT License
3.07k stars 271 forks source link

How to verify that element has no children (alternatively, render result is `null`'ish) #1544

Closed erheron closed 9 months ago

erheron commented 9 months ago

Ask your Question

I have a component which renders null if some data validation check do not pass. I'd like to test this behavior using RNTL. Question: how do I do this? For the record, we are using a custom renderWithProviders function, as suggested in Redux docs

I thought about wrapping the element I'm testing in a <View testId="artificial_root_view"> and then somehow verifying that this view has no children. But when I tried to do the following 👇🏻

expect(screen.getByTestId("artificial_root_view").children).toBeNull();

I got the following error:

expect(received).toBeNull()
    Received: [{"_fiber": [FiberNode]}]
mdjastrzebski commented 9 months ago

@erheron Could you post a minimal repro for your issue?

mdjastrzebski commented 9 months ago

You should not inspect children for such purpose, as they hold some internal types. Try using toBeEmptyElement() matcher.

erheron commented 9 months ago

Yeah, toBeEmptyElement seems to be working 🤔

erheron commented 9 months ago

For the record, this is what I had in mind (omitting the whole renderWithProviders topic):

interface Props {
    username: string;
}

function Component(props: Props) {
    if (props.username === '') {
        return null;
    }

    return (
        <View testID="main_component_view">
            <Text> {props.username} </Text>
        </View>
    );
}

describe('Test Component', () => {
    test('Renders null if username is an empty string', () => {
        render(<Component username="" />);

        // WHAT TO EXPECT HERE?
    });
});
erheron commented 9 months ago

It would be nice if the jest-native matchers contained more inline documentation 🙏🏻

This is what it looks like in my node_modules rn -- only toBeOnTheScreen contains the explanation 😒

export interface JestNativeMatchers<R> {
    /**
     * Assert whether an element is present in the element tree or not.
     */
    toBeOnTheScreen(): R;
    toBeChecked(): R;
    toBeCollapsed(): R;
    toBeDisabled(): R;
    toBeBusy(): R;
    toBeEmptyElement(): R;
    toBeEnabled(): R;
    toBeExpanded(): R;
    toBePartiallyChecked(): R;
    toBeSelected(): R;
    toBeVisible(): R;
    toContainElement(element: ReactTestInstance | null): R;
    toHaveAccessibilityValue(expectedValue: AccessibilityValueMatcher): R;
    toHaveAccessibleName(expectedName?: TextMatch, options?: TextMatchOptions): R;
    toHaveDisplayValue(expectedValue: TextMatch, options?: TextMatchOptions): R;
    toHaveProp(name: string, expectedValue?: unknown): R;
    toHaveStyle(style: StyleProp<Style>): R;
    toHaveTextContent(expectedText: TextMatch, options?: TextMatchOptions): R;
}
erheron commented 9 months ago

Let's close this -- toBeEmptyElement does what I need. Sorry for hustle 🙇🏻

mdjastrzebski commented 9 months ago

Would screen.toBeNull() work?

describe('Test Component', () => {
    test('Renders null if username is an empty string', () => {
        render(<Component username="" />);
                 screen.root.toBeNull();
    });
});
erheron commented 9 months ago

Yeah I think it would.

I actually ended up implementing a custom util very similar to what you wrote:


/// file TestingUtils.tsx
export function expectScreenToBeEmpty(){
   expect(screen.getByTestId('artificial_root')).toBeEmptyElement();
}

/// file renderWithProviders.tsx

function renderWithProviders(ui, options){

  const Wrapper = <View testID="artificial_root>; // and more providers

  return render(ui, { wrapper: Wrapper, options});

}

/// file test.tsx

test('Test empty render', () => {
  TestUtils.expectScreenToBeEmpty;
}
mdjastrzebski commented 9 months ago

@erheron whatever floats your boat ⛵️

I think that screen.root.toBeNull() is the most idiomatic approach (assuming it works 🤣). It says that the component you rendered resulted in null (non-existent) root host component.

re missing TSDocs: yeah, I need to add them 👍🏻

guvenkaranfil commented 9 months ago

But instead of checking the nullability of the component why not check that the string you would look for is not there by queryByText. I just wondered about your case to broad my perspective @erheron

mdjastrzebski commented 9 months ago

@guvenkaranfil There are some cases when checking for empty render or empty children might be better, e.g., when testing a component displaying a notification dot in the UI. It does not have a text representation, and you want to check whether the element is rendered depending on the notification state. As there are many ways to test things, you could also check for tesiID in such cases.

guvenkaranfil commented 9 months ago

Hmm makes sense thanks for the reply @mdjastrzebski

erheron commented 9 months ago

But instead of checking the nullability of the component why not check that the string you would look for is not there by queryByText. I just wondered about your case to broad my perspective @erheron

@guvenkaranfil imagine a component, originally looking like this:

function Component(props){
   if(!validate(props)){ return null; }

   return (<View testID="component_root"> .... </View>);
}

Now, writing a test which checks that e.g. screen.getByTestID("component_root") is not on the screen, will not get us far -- how do I really know what is on a screen? I have to be sure that it's null.

Moreover, a component may change in the future to something like this:

function Component(newProps){
   if(!anotherValidate(newProps)){ return <PlaceholderUI />; }

   return (<View testID="component_root"> .... </View>);
}

Now, a test which checks for null'ish render would "catch" this change and that test would probably need to be updated. On the contrary, a test which checks that something is not on the screen, wouldn't reflect what this component is doing anymore

guvenkaranfil commented 9 months ago

Thanks for detailed explanation. Yeap it much more make sense that we might want to check nullability

erheron commented 9 months ago

@erheron whatever floats your boat ⛵️

I think that screen.root.toBeNull() is the most idiomatic approach (assuming it works 🤣). It says that the component you rendered resulted in null (non-existent) root host component.

re missing TSDocs: yeah, I need to add them 👍🏻

@mdjastrzebski oh right, screen.root.toBeNull of course won't work, since we render some providers in our renderWithProviders function.