tamagui / tamagui

Style React fast with 100% parity on React Native, an optional UI kit, and optimizing compiler.
https://tamagui.dev
MIT License
10.23k stars 415 forks source link

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

Open mfrfinbox opened 1 month ago

mfrfinbox commented 1 month ago

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 https://github.com/callstack/react-native-testing-library/issues/1566#issuecomment-2087058533

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 commented 1 month ago

@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 commented 1 month ago

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: https://github.com/tamagui/tamagui/issues/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 commented 1 month ago

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

DimitarNestorov commented 2 weeks ago

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 commented 2 weeks ago

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 commented 2 weeks ago

@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 commented 2 weeks ago

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

Shouldn't that be responsibility of RN Jest preset?