Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Can't use user events from React Native Testing Library since Tamagui elements won't fire the correct events #2614

Open
mfrfinbox opened this issue May 1, 2024 · 7 comments

Comments

@mfrfinbox
Copy link

Current Behavior

While testing my components I am trying to use user events https://callstack.github.io/react-native-testing-library/docs/user-event

// This wont work
const user = userEvent.setup();
await user.press(button);

// This works!
fireEvent.press(button);

I am trying to use user event because it resembles the user interaction, I've opened an issue on RNTL github but they believe the issue might be on this side and since Tamagui codebase is complex they couldn't figue this out, here's the link to the comment I've made callstack/react-native-testing-library#1566 (comment)

Expected Behavior

user event should trigger as expected

Tamagui Version

^1.89.29

Platform (Web, iOS, Android)

All

Reproduction

import { render, screen } from "@testing-library/react-native";
import { TamaguiProvider } from "tamagui";

import MyComponent from "../myComponent";
import { config } from "../tamagui.config";

describe("MyComponent", () => {
  it("Check all checkboxes and see the agree button get enabled", () => {
    render(
      <TamaguiProvider config={config}>
        <MyComponent />
      </TamaguiProvider>,
    );

    // All good here, it selects fine
    const checkbox1 = screen.getByTestId("terms-1");
    const checkbox2 = screen.getByTestId("terms-2");
    const checkbox3 = screen.getByTestId("terms-3");
    const checkbox4 = screen.getByTestId("terms-4");

    const user = userEvent.setup();
    await user.press(element);

    // Nothing happens
    user.press(checkbox1);
    user.press(checkbox2);
    user.press(checkbox3);
    user.press(checkbox4);

    // This is the way Tamagui handles the disabled state
    expect(screen.getByRole("button")).toBeEnabled;
  });
});

System Info

System:
    OS: macOS 14.4.1
    CPU: (12) arm64 Apple M2 Pro
    Memory: 71.98 MB / 16.00 GB
    Shell: 5.9 - /bin/zsh
  Binaries:
    Node: 20.11.0 - ~/.nvm/versions/node/v20.11.0/bin/node
    Yarn: 4.0.1 - ~/.nvm/versions/node/v20.11.0/bin/yarn
    npm: 10.5.0 - ~/WebstormProjects/ppa-mobile/node_modules/.bin/npm
    pnpm: 8.15.4 - ~/Library/pnpm/pnpm
    Watchman: 2024.03.18.00 - /opt/homebrew/bin/watchman
  Browsers:
    Chrome: 124.0.6367.118
    Edge: 124.0.2478.67
    Safari: 17.4.1
  npmPackages:
    @aptabase/react-native: ^0.3.9 => 0.3.9 
    @babel/core: ^7.20.0 => 7.24.3 
    @babel/preset-typescript: ^7.24.1 => 7.24.1 
    @clerk/clerk-expo: ^0.20.5 => 0.20.10 
    @date-fns/utc: ^1.2.0 => 1.2.0 
    @dev-plugins/react-query: ^0.0.5 => 0.0.5 
    @expo-google-fonts/sofia-sans-extra-condensed: ^0.2.3 => 0.2.3 
    @expo-google-fonts/work-sans: ^0.2.3 => 0.2.3 
    @gorhom/bottom-sheet: ^4.6.1 => 4.6.1 
    @hookform/resolvers: ^3.3.4 => 3.3.4 
    @react-native-async-storage/async-storage: 1.21.0 => 1.21.0 
    @sentry/react-native: 5.19.1 => 5.19.1 
    @shopify/react-native-skia: 0.1.221 => 0.1.221 
    @supabase/supabase-js: ^2.39.6 => 2.39.8 
    @tamagui/animations-css: ^1.90.2 => 1.92.1 
    @tamagui/config: ^1.89.29 => 1.92.1 
    @tamagui/core: ^1.90.2 => 1.92.1 
    @tamagui/create-theme: ^1.90.2 => 1.92.1 
    @tamagui/list-item: ^1.90.2 => 1.92.1 
    @tamagui/lucide-icons: ^1.90.2 => 1.92.1 
    @tamagui/metro-plugin: ^1.89.29 => 1.92.1 
    @tamagui/react-native-svg: ^1.90.2 => 1.92.1 
    @tamagui/toast: ^1.90.2 => 1.92.1 
    @tanstack/eslint-plugin-query: ^5.20.1 => 5.28.6 
    @tanstack/react-query: ^5.21.7 => 5.28.6 
    @tanstack/react-query-devtools: ^5.21.7 => 5.28.6 
    @testing-library/jest-native: ^5.4.3 => 5.4.3 
    @testing-library/react-native: ^12.4.5 => 12.4.5 
    @types/jest: ^29.5.12 => 29.5.12 
    @types/jsonwebtoken: ^9.0.5 => 9.0.6 
    @types/react: ~18.2.45 => 18.2.69 
    @types/react-test-renderer: ^18.0.7 => 18.0.7 
    @wuba/react-native-echarts: ^1.2.5 => 1.3.0 
    ably: ^1.2.49 => 1.2.50 
    axios: ^1.6.8 => 1.6.8 
    burnt: ^0.12.2 => 0.12.2 
    change-case: ^5.4.3 => 5.4.3 
    date-fns: ^3.3.1 => 3.6.0 
    echarts: ^5.5.0 => 5.5.0 
    env-cmd: ^10.1.0 => 10.1.0 
    eslint: ^8.56.0 => 8.57.0 
    eslint-config-universe: ^12.0.0 => 12.0.0 
    eslint-plugin-testing-library: ^6.2.2 => 6.2.2 
    expo: ~50.0.13 => 50.0.14 
    expo-application: ~5.8.3 => 5.8.3 
    expo-av: ~13.10.5 => 13.10.5 
    expo-build-properties: ~0.11.1 => 0.11.1 
    expo-constants: ~15.4.5 => 15.4.5 
    expo-dev-client: ~3.3.11 => 3.3.11 
    expo-device: ~5.9.3 => 5.9.3 
    expo-document-picker: ~11.10.1 => 11.10.1 
    expo-font: ~11.10.3 => 11.10.3 
    expo-image-picker: ~14.7.1 => 14.7.1 
    expo-jwt: ^1.7.0 => 1.7.1 
    expo-linear-gradient: ~12.7.2 => 12.7.2 
    expo-linking: ~6.2.2 => 6.2.2 
    expo-notifications: ~0.27.6 => 0.27.6 
    expo-router: ~3.4.8 => 3.4.8 
    expo-secure-store: ^12.8.1 => 12.8.1 
    expo-status-bar: ~1.11.1 => 1.11.1 
    expo-updates: ~0.24.12 => 0.24.12 
    i: ^0.3.7 => 0.3.7 
    jest: ^29.3.1 => 29.7.0 
    jest-expo: ~50.0.4 => 50.0.4 
    jotai: ^2.7.1 => 2.7.1 
    jsonwebtoken: ^9.0.2 => 9.0.2 
    mime: ^4.0.1 => 4.0.1 
    moti: ^0.28.1 => 0.28.1 
    native-notify: ^4.0.0 => 4.0.0 
    npm: ^10.5.0 => 10.5.0 
    postgres: ^3.4.3 => 3.4.4 
    prettier: ^3.2.5 => 3.2.5 
    react: 18.2.0 => 18.2.0 
    react-dom: 18.2.0 => 18.2.0 
    react-hook-form: ^7.51.0 => 7.51.1 
    react-native: 0.73.6 => 0.73.6 
    react-native-circular-progress: ^1.3.9 => 1.3.9 
    react-native-dialog: ^9.3.0 => 9.3.0 
    react-native-dotenv: ^3.4.10 => 3.4.11 
    react-native-gesture-handler: ~2.14.0 => 2.14.1 
    react-native-progress: ^5.0.1 => 5.0.1 
    react-native-reanimated: ~3.6.2 => 3.6.3 
    react-native-responsive-screen: ^1.4.2 => 1.4.2 
    react-native-safe-area-context: 4.8.2 => 4.8.2 
    react-native-screens: ~3.29.0 => 3.29.0 
    react-native-size-matters: ^0.4.2 => 0.4.2 
    react-native-svg: ^14.1.0 => 14.1.0 
    react-native-ui-lib: ^7.17.2 => 7.18.3 
    react-native-url-polyfill: ^2.0.0 => 2.0.0 
    react-native-web: ~0.19.6 => 0.19.10 
    react-native-webview: 13.6.4 => 13.6.4 
    react-native-youtube-iframe: ^2.3.0 => 2.3.0 
    react-test-renderer: 18.2.0 => 18.2.0 
    rn-tourguide: ^3.3.0 => 3.3.0 
    tamagui: ^1.89.29 => 1.92.1 
    typescript: ^5.1.3 => 5.4.3 
    vexo-analytics: ^1.3.13 => 1.3.13 
    zod: ^3.22.4 => 3.22.4
@mdjastrzebski
Copy link

@mfrfinbox the repro scenario is missing MyComponent source .

The root cause why RNTL User Event is not invoking press event is that in the rendered host components tree, there is no press-related handler (onPress, onPressIn, etc). More interestingly, there is no event handler at all(!), which makes me think how does it work. Perhaps there is some mocking (by Tamagui, or RN) that removes the event handlers but that's just a hint.

See host element tree from the original RNTL issue.

@mfrfinbox
Copy link
Author

mfrfinbox commented May 2, 2024

I've update the repro scenario, thanls for pointing this out @mdjastrzebski , also I can make it work like this but it feels very hacky and I would likle to use user events instead if possible:

import { fireEvent, render, screen } from "@testing-library/react-native";
import { useState } from "react";
import { Button, Checkbox, SizableText, TamaguiProvider, View } from "tamagui";

import { config } from "../tamagui.config";

const MyComponent = () => {
  const [allAgreed, setAllAgreed] = useState(false);
  const handleAgree = (agreed: boolean) => {
    agreed && setAllAgreed(agreed);
  };

  return (
    <View>
      <Checkbox
        id="checkbox-1"
        testID="checkbox-1"
        size="$3"
        onCheckedChange={handleAgree}
      >
        <Checkbox.Indicator>
          <SizableText>X</SizableText>
        </Checkbox.Indicator>
      </Checkbox>
      <Button accessible disabled={!allAgreed}>
        Agree
      </Button>
    </View>
  );
};

describe("Reproducible Tamagui issue #2613", () => {
  it("Enables button after checking the checkbox", () => {
    render(
      <TamaguiProvider config={config}>
        <MyComponent />
      </TamaguiProvider>,
    );

    const checkbox1 = screen.getByTestId("checkbox-1");
    fireEvent.press(checkbox1);

    // This is the way Tamagui handles the disabled state I believe
    expect(screen.getByRole("button")).not.toHaveProp("pointerEvents", "none");
  });
});

Additionally if you read the description of the problem there are 2 issues:

  1. Can't select by role (Thats why I used testID) see: Can't access to elements using by role React native testing library due to a lack of accesibility attribute #2613
  2. User event is not triggered (Something to do how Tamagui is wired under the hood I believe)

Please let me know if you need more info

@ehxxn
Copy link
Member

ehxxn commented May 2, 2024

thanks for the details, I'll check this tommorow
a minimal re-production using Tamagui starters can be very helpful

@DimitarNestorov
Copy link
Contributor

I also had an issue with using user events from RNTL with Tamagui components. Looks like the issue is caused by the fact that Jest doesn't account for the exports field in package.json and imports index.js instead of index.native.js. I ended up making a custom resolver for Jest as a workaround:

/**
 * Custom Jest resolver
 * @param {string} path
 * @param {import('jest-resolve').ResolverOptions} options
 * @returns {string}
 */
exports.sync = function nativeResolver(path, options) {
  const defaultResolverResult = options.defaultResolver(path, options);

  if (path.startsWith('@tamagui')) {
    return defaultResolverResult.replace('index.js', 'index.native.js');
  }

  return defaultResolverResult;
};

@Cowner
Copy link

Cowner commented May 15, 2024

Any updates on this? I'm currently unable to test a user journey due to being unable to check the required checkboxes.

Tried @DimitarNestorov's solution which resulted in:

Test suite failed to run ENOENT: no such file or directory, open '.../node_modules/@tamagui/polyfill-dev/index.native.js'

Anymore information on how you made the custom resolver work?

@DimitarNestorov
Copy link
Contributor

DimitarNestorov commented May 16, 2024

@Cowner we don't have an import for @tamagui/polyfill-dev. You should be able to modify the resolver to check if the file exists before the return or if @tamagui/polyfill-dev is a part of the path.

  if (path.startsWith('@tamagui') && !path.includes("@tamagui/polyfill-dev") {

@mdjastrzebski
Copy link

@DimitarNestorov could you explain details what your custom resolver does?

Shouldn't that be responsibility of RN Jest preset?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants