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

使用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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 05:54:56