使用Playwright无法选中Select组件选项的问题排查
问题:React+Flowbite-React国家选择组件在Playwright测试中无法选中选项
我开发了一个基于React+flowbite-react的CountrySelector组件,该组件从API加载国家列表并填充到Select元素中,UI运行正常,可渲染出<option value="France">France</option>这类选项。父组件使用该组件时传入了textAsValue等属性,但在Playwright自动化测试中,执行page.selectOption('[data-testid="companyBillingCountry"]', 'France')时出现30000ms超时错误,提示找不到对应选项,无法完成选择操作。
组件代码
import { CountryDto } from '@/lib/models'; import { CountryService } from '@/services'; import { Select } from 'flowbite-react'; import React, { ChangeEvent, ChangeEventHandler, useEffect, useState } from 'react'; type CountrySelectorProps = { value?: string; onChange?: ChangeEventHandler<HTMLSelectElement>; id?: string; name?: string; isRequired?: boolean; isDisabled?: boolean; textAsValue?: boolean; selectedCountry?: CountryDto; "data-testid"?: string }; const CountrySelector = ({ value, onChange, id, name, isRequired, isDisabled, textAsValue, selectedCountry, "data-testid": dataTestId }: CountrySelectorProps) => { const [countries, setCountries] = useState<CountryDto[]>([]); const [selectedValue, setSelectedValue] = useState<string>(value || ''); const [isLoading, setIsLoading] = useState(true); useEffect(() => { setIsLoading(true); CountryService.getCountries().then(result => { if (result.success) { setCountries(result.data || []); } setIsLoading(false); }).catch(() => { setIsLoading(false); }); }, []); useEffect(() => { setSelectedValue(value || (selectedCountry ? (textAsValue ? selectedCountry.name : selectedCountry.iso2Code) : '') || ''); }, [value, selectedCountry, textAsValue]); const handleChange = (event: ChangeEvent<HTMLSelectElement>) => { const selectedCountryValue = event.target.value; setSelectedValue(selectedCountryValue); onChange && onChange(event); }; return ( <Select id={id} name={name} value={selectedValue} onChange={handleChange} required={isRequired} disabled={isDisabled || isLoading} data-testid={dataTestId} > {isLoading ? ( <option value="">Loading...</option> ) : ( <> <option value="">{selectedCountry ? selectedCountry.name : 'Select a country'}</option> {countries.map(country => ( <option key={country.iso2Code} value={textAsValue ? country.name as string : country.iso2Code as string} > {country.name} </option> ))} </> )} </Select> ); }; export default CountrySelector;
父组件使用代码
<CountrySelector value={companyBillingDetails.companyBillingCountry} data-testid="companyBillingCountry" onChange={event => setCompanyBillingDetails({ ...companyBillingDetails, companyBillingCountry: event.target.value, }) } isRequired textAsValue isDisabled={isSubmitting} />
测试代码
test('should handle new client billing details', async ({ page }) => { // Fill out billing details // Wait for country selector to load and be interactable await page.waitForSelector('[data-testid="companyBillingCountry"]:not([disabled])', { timeout: 15000 }); // Select country await page.selectOption('[data-testid="companyBillingCountry"]', 'France'); // Submit the form const submitButton = await page.getByTestId('submit-job-button'); await submitButton.click(); // Check for success toast const successToast = await page.getByText('Job submitted successfully'); expect(successToast).toBeDefined(); });
报错信息
Error: page.selectOption: Test timeout of 30000ms exceeded. Call log: - waiting for locator('[data-testid="companyBillingCountry"]') - locator resolved to <select required="" data-testid="companyBillingCountry" class="block w-full border disabled:cursor-not-allowed disabled:opacity-50 border-gray-300 bg-gray-50 text-gray-900 focus:border-cyan-500 focus:ring-cyan-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400 dark:focus:border-cyan-500 dark:focus:ring-cyan-500 p-2.5 text-sm rounded-lg">…</select> - attempting select option action 2 × waiting for element to be visible and enabled - did not find some options - retrying select option action - waiting 20ms 2 × waiting for element to be visible and enabled - did not find some options - retrying select option action - waiting 100ms 55 × waiting for element to be visible and enabled - did not find some options - retrying select option action - waiting 500ms 133 | 134 | // Select country > 135 | await page.selectOption('[data-testid="companyBillingCountry"]', 'France'); | ^ 136 |
排查原因
- 等待逻辑不完整:当前测试仅等待组件解除禁用,但未确保国家列表数据已加载完成。组件解除禁用后,可能国家选项还未渲染到DOM中。
- 选项匹配时机问题:Playwright执行选择操作时,目标选项可能还未从API加载完成并渲染,导致找不到对应选项。
- 状态同步延迟:组件初始化时
selectedValue的更新未关联国家列表加载状态,可能导致Select组件的value与实际渲染的选项不一致,干扰选择逻辑。
解决方案
1. 优化测试等待逻辑,确保目标选项存在
修改测试代码,在选择前等待目标选项渲染完成:
test('should handle new client billing details', async ({ page }) => { // 等待组件可用 await page.waitForSelector('[data-testid="companyBillingCountry"]:not([disabled])', { timeout: 15000 }); // 额外等待"France"选项出现在DOM中 await page.waitForSelector('[data-testid="companyBillingCountry"] option[value="France"]', { timeout: 10000 }); // 选择国家 await page.selectOption('[data-testid="companyBillingCountry"]', 'France'); // 提交表单 const submitButton = await page.getByTestId('submit-job-button'); await submitButton.click(); // 检查成功提示 const successToast = await page.getByText('Job submitted successfully'); expect(successToast).toBeDefined(); });
2. 明确通过文本匹配选项
如果担心value属性匹配问题,可指定通过选项文本进行选择:
await page.selectOption('[data-testid="companyBillingCountry"]', { label: 'France' });
3. 优化组件状态同步逻辑
调整组件中selectedValue的更新时机,确保国家列表加载完成后再同步传入的value:
// 修改第二个useEffect,增加countries依赖 useEffect(() => { if (countries.length === 0) return; // 列表未加载时跳过更新 setSelectedValue(value || (selectedCountry ? (textAsValue ? selectedCountry.name : selectedCountry.iso2Code) : '') || ''); }, [value, selectedCountry, textAsValue, countries]);
4. Mock API请求(推荐用于测试环境)
在测试中Mock国家列表接口,避免真实API的网络延迟影响测试稳定性:
test('should handle new client billing details', async ({ page }) => { // Mock国家列表接口,直接返回测试所需数据 await page.route('**/api/countries', async (route) => { await route.fulfill({ json: { success: true, data: [ { iso2Code: 'FR', name: 'France' }, // 其他需要的测试国家数据 ] } }); }); // 等待组件可用并选择国家 await page.waitForSelector('[data-testid="companyBillingCountry"]:not([disabled])', { timeout: 15000 }); await page.selectOption('[data-testid="companyBillingCountry"]', 'France'); // 后续测试步骤... });
内容的提问来源于stack exchange,提问作者Chris Claude
相关产品推荐
相关产品推荐

