You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

添加gluestack-ui v2 Select组件后@testing-library测试失败

React Native中添加gluestack-ui v2 Select组件后,@testing-library/react-native无法通过文本查找元素

添加gluestack-ui v2的Select组件后,原有的@testing-library/react-native测试无法通过文本查找元素,移除Select组件后测试恢复正常。


添加Select组件前的正常状态

test-utils.tsx

import React from 'react'
import {render} from '@testing-library/react-native'
import { GluestackUIProvider } from "@/components/ui/gluestack-ui-provider";
import fs from 'fs';

const AllTheProviders = ({children}) => {
  return (
    <GluestackUIProvider>
        {children}
    </GluestackUIProvider>
  )
}

const customRender = (ui, options?) => {
  const view = render(ui, {wrapper: AllTheProviders, ...options})

  const style = document.createElement('style');
  style.innerHTML = fs.readFileSync('./output.css', 'utf8');
  document.head.appendChild(style);

  return view;
}
// re-export everything
export * from '@testing-library/react-native'

// override render method
export {customRender as render}

原组件代码

import React, { useState } from 'react';
import { View, FlatList } from 'react-native';
import { Text } from '@/components/ui/text';
import { Input, InputField } from '@/components/ui/input';
import { Heading } from '../ui/heading';

export const JamSessionsBanner = ({ sessions }) => {
  const [lieu, setLieu] = useState('');
  const [date, setDate] = useState('');

  const filteredSessions = sessions.filter((session) => {
    const matchesLieu = lieu ? session.lieu.toLowerCase().includes(lieu.toLowerCase()) : true;
    const matchesDate = date ? new Date(session.date).toDateString() === new Date(date).toDateString() : true;
    return matchesLieu && matchesDate;
  });

  return (
    <View style={{ padding: 16 }}>
      <Heading>Upcoming JAM Sessions</Heading>
      <FlatList
        data={filteredSessions}
        keyExtractor={(item) => item.id}
        renderItem={({ item }) => (
          <View style={{ marginBottom: 16 }}>
            <Text>{item.lieu}, {item.ville}</Text>
            <Text>{new Date(item.date).toLocaleString()}</Text>
          </View>
        )}
      />
    </View>
  );
};

模拟数据

export const mockSessions = [
  {
    id: '1',
    date: '2024-10-18T19:30:00',
    lieu: 'Le Café',
    ville: 'Paris',
    description: 'Session de jam ouverte à tous.',
    musiciens: ['John Doe', 'Jane Smith'],
    popularité: 100,
    genre: 'reggae',
  },
  {
    id: '2',
    date: '2024-10-20T20:00:00',
    lieu: 'La Ska Factory',
    ville: 'Lyon',
    description: 'Jam session ska.',
    musiciens: ['Paul Dupont', 'Marie Lafont'],
    popularité: 80,
    genre: 'ska',
  },
];

Jest配置

module.exports = {
  preset: 'react-native',
  transform: {
    '^.+\\.tsx?$': 'babel-jest',
  },
  setupFilesAfterEnv: ['<rootDir>/setupTests.ts'],
  moduleNameMapper: {
    '\\.(jpg|jpeg|png|gif|svg)$': 'jest-transform-stub',
    '\\.(css|less|scss|sass)$': 'identity-obj-proxy',
  },
  testEnvironment: 'jsdom',
  testPathIgnorePatterns: [
    '<rootDir>/node_modules/',
    '<rootDir>/e2e/',
    "node_modules/(?!((jest-)?react-native|@react-native(-community)?)|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|@gluestack-ui/*|@gluestack-style/*|@legendapp/*|react-native-svg)"
  ],
};

可通过的测试代码

import { render, screen } from '../../test-utils';
import { JamSessionsBanner } from './JamSessionsBanner';
import { mockSessions } from './jam.mock';

test('renders sessions correctly', () => {
  render(<JamSessionsBanner sessions={mockSessions} />);
  expect(screen.getByText(/Le Café, Paris/i)).toBeTruthy();
});

添加Select组件后的问题

修改后的组件代码

import React, { useState } from 'react';
import { View, FlatList } from 'react-native';
import { Text } from '@/components/ui/text';
import { Input, InputField } from '@/components/ui/input';
import { Heading } from '../ui/heading';
import {
  Select,
  SelectBackdrop,
  SelectContent,
  SelectIcon,
  SelectInput,
  SelectItem,
  SelectPortal,
  SelectTrigger
} from '@/components/ui/select';
import { ChevronDownIcon } from '../ui/icon';

export const JamSessionsBanner = ({ sessions }) => {
  const [lieu, setLieu] = useState('');
  const [date, setDate] = useState('');
  const [selectedVille, setSelectedVille] = useState('Partout');
  const villes = ['Partout', ...new Set(sessions.map(session => session.ville))];

  const filteredSessions = sessions.filter((session) => {
    const matchesLieu = lieu ? session.lieu.toLowerCase().includes(lieu.toLowerCase()) : true;
    const matchesDate = date ? new Date(session.date).toDateString() === new Date(date).toDateString() : true;
    return matchesLieu && matchesDate;
  });

  return (
    <View style={{ padding: 16 }}>
      <Heading>
        Upcoming JAM Sessions
        <Select selectedValue={selectedVille} onValueChange={setSelectedVille}>
          <SelectTrigger variant="outline">
            <SelectInput placeholder="Filter by city" />
            <SelectIcon as={ChevronDownIcon} />
          </SelectTrigger>
          <SelectPortal>
            <SelectBackdrop />
            <SelectContent>
              {villes.map(ville => (
                <SelectItem key={ville} label={ville} value={ville} />
              ))}
            </SelectContent>
          </SelectPortal>
        </Select>
      </Heading>
      <FlatList
        data={filteredSessions}
        keyExtractor={(item) => item.id}
        renderItem={({ item }) => (
          <View style={{ marginBottom: 16 }}>
            <Text>{item.lieu}, {item.ville}</Text>
            <Text>{new Date(item.date).toLocaleString()}</Text>
          </View>
        )}
      />
    </View>
  );
};

测试报错信息

Unable to find an element with text: /Le Café, Paris/i

完整控制台输出

FAIL  components/JamSessionsBanner/JamSessionsBanner.test.tsx (5.291 s)
  JamSessionsBanner
    ✕ renders sessions correctly (726 ms)

  ● JamSessionsBanner › renders sessions correctly

    Unable to find an element with text: /Le Café, Paris/i

    <View>
      <View>
        <RCTText
          accessibilityRole="header"
          accessible={true}
        >
          Upcoming JAM Sessions
          <View>
            <View
              accessible={true}
              role="button"
            >
              <TextInput
                aria-hidden={true}
                importantForAccessibility="no"
                placeholder="Filter by city"
                value="Partout"
              />
              <RNSVGSvgView
                role="img"
              >
                <RNSVGGroup>
                  <RNSVGGroup>
                    <RNSVGPath />
                  </RNSVGGroup>
                </RNSVGGroup>
              </RNSVGSvgView>
            </View>
          </View>
        </RCTText>
        <RCTScrollView>
          <RCTScrollContentView>
            <View>
              <View>
                <RCTText
                  accessible={true}
                >
                  Le Café
                  , 
                  Paris
                </RCTText>
                <RCTText
                  accessible={true}
                >
                  18/10/2024 19:30:00
                </RCTText>
              </View>
            </View>
            <View>
              <View>
                <RCTText
                  accessible={true}
                >
                  La Ska Factory
                  , 
                  Lyon
                </RCTText>
                <RCTText
                  accessible={true}
                >
                  20/10/2024 20:00:00
                </RCTText>
              </View>
            </View>
          </RCTScrollContentView>
        </RCTScrollView>
      </View>
    </View>

      10 |     render(<JamSessionsBanner sessions={mockSessions} />);
      11 |     // expect(screen.getByText(/Session de jam ouverte à tous/i)).toBeTruthy();
    > 12 |     expect(screen.getByText(/Le Café, Paris/i)).toBeTruthy();
         |                   ^
      13 |   });
      14 |
      15 |   // test('displays empty state when no sessions are available', async () => {

      at Object.getByText (components/JamSessionsBanner/JamSessionsBanner.test.tsx:12:19)
      at asyncGeneratorStep (node_modules/@babel/runtime/helpers/asyncToGenerator.js:3:17)
      at asyncGeneratorStep (node_modules/@babel/runtime/helpers/asyncToGenerator.js:17:9)
      at _next (node_modules/@babel/runtime/helpers/asyncToGenerator.js:22:7)
      at Object.<anonymous> (node_modules/@babel/runtime/helpers/asyncToGenerator.js:14:12)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 total
Snapshots:   0 total
Time:        6.34 s

已尝试的操作

  • 确认@testing-library/react-native配置正确,自定义render包含必要的上下文提供者
  • 在测试中添加waitFor方法
  • 确认仅添加Select组件后出现问题,手动测试Select功能正常

排查思路与解决方案

核心原因

从控制台输出可见,目标文本Le Café, Paris被拆分为三个独立的文本节点:Le Café、, 、Paris。@testing-library/react-native的getByText默认匹配单个完整文本节点,因此正则表达式无法匹配分散的文本内容。添加Select组件后,组件结构变化导致测试库不再自动合并相邻文本节点的内容,从而触发该问题。

解决方案

方案1:修改测试的文本匹配逻辑

使用回调函数匹配文本内容,只要文本节点包含目标片段即可:

test('renders sessions correctly', () => {
  render(<JamSessionsBanner sessions={mockSessions} />);
  expect(screen.getByText((text) => text.includes('Le Café') && text.includes('Paris'))).toBeTruthy();
});

方案2:调整组件中的文本拼接方式

将多个变量拼接为单个字符串,避免生成多个文本节点:

// 组件中修改Text组件的内容
<Text>{`${item.lieu}, ${item.ville}`}</Text>

方案3:通过父容器定位后匹配

给每个会话项添加testID,再通过within限定查找范围,分别匹配片段文本:

// 组件中添加testID
<View style={{ marginBottom: 16 }} testID={`session-item-${item.id}`}>
  <Text>{item.lieu}, {item.ville}</Text>
  <Text>{new Date(item.date).toLocaleString()}</Text>
</View>

// 测试中修改查找逻辑
test('renders sessions correctly', () => {
  render(<JamSessionsBanner sessions={mockSessions} />);
  const firstSession = screen.getByTestId('session-item-1');
  expect(within(firstSession).getByText('Le Café')).toBeTruthy();
  expect(within(firstSession).getByText('Paris')).toBeTruthy();
});

内容的提问来源于stack exchange,提问作者Loann Delgado

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.17 04:49:51