添加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
相关产品推荐
相关产品推荐

